Skip to content

Serverless Best Practices — Production-Ready Serverless

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Serverless Best Practices. We cover key concepts, practical examples, and best practices to help you master this topic.

Serverless best practices cover function design, error handling, performance optimization, cost management, Observability, testing, and operational patterns for production serverless applications.

What You'll Learn

By the end of this lesson you will understand best practices for designing, building, deploying, and operating serverless applications in production with reliability, performance, and cost efficiency.

Why It Matters

Serverless applications that start small can become expensive, slow, or unreliable without proper practices. Following established patterns prevents common production issues and ensures your application scales gracefully.

Real-World Use

DodaZIP's serverless conversion pipeline follows all serverless best practices: single-responsibility functions, connection reuse, structured logging, cost monitoring, and automated testing with local emulation.

flowchart TD
    BP[Best Practices] --> D["Single Responsibility"]
    BP --> C["Connection Reuse"]
    BP --> L["Structured Logging"]
    BP --> T["Automated Testing"]
    BP --> CO["Cost Monitoring"]
    BP --> ER["Error Handling"]
    BP --> P["Performance Tuning"]
    style BP fill:#f90,color:#fff

Function Design

Each function should do one thing. Keep handlers thin. Initialize connections outside the handler.

# function_design.py
# Best practice function design

import json

# Good: connection initialized once, reused across invocations
db_client = None

def get_db_client():
    global db_client
    if db_client is None:
        db_client = {"connected": True}
        print("  Initialized database connection")
    return db_client

def lambda_handler(event, context):
    db = get_db_client()
    user_id = event.get("pathParameters", {}).get("id")
    print(f"Fetching user {user_id}")
    
    return {
        "statusCode": 200,
        "body": json.dumps({"id": user_id, "name": "Alice"})
    }

print("Invocation 1:")
print(lambda_handler({"pathParameters": {"id": "1"}}, None)["body"])
print("\nInvocation 2 (reuses connection):")
print(lambda_handler({"pathParameters": {"id": "2"}}, None)["body"])

Expected output:

Invocation 1:
  Initialized database connection
Fetching user 1
{"id": "1", "name": "Alice"}

Invocation 2 (reuses connection):
Fetching user 2
{"id": "2", "name": "Alice"}

Error Handling

Handle all error types, implement dead-letter queues, and return meaningful error responses.

# error_handling_best.py
# Error handling best practices

import json
import traceback

class AppError(Exception):
    def __init__(self, message, status_code=400):
        self.message = message
        self.status_code = status_code

def lambda_handler(event, context):
    try:
        body = json.loads(event.get("body", "{}"))
        
        if not body.get("email"):
            raise AppError("Email is required", 400)
        
        result = process_user(body)
        return {"statusCode": 200, "body": json.dumps(result)}
    
    except json.JSONDecodeError:
        return {"statusCode": 400, "body": json.dumps({"error": "Invalid JSON"})}
    except AppError as e:
        return {"statusCode": e.status_code, "body": json.dumps({"error": e.message})}
    except Exception as e:
        print(f"Unhandled error: {traceback.format_exc()}")
        return {"statusCode": 500, "body": json.dumps({"error": "Internal server error"})}

def process_user(data):
    return {"email": data["email"], "status": "created"}

tests = [
    {"body": "not json"},
    {"body": '{}'},
    {"body": '{"email": "alice@example.com"}'},
]
for t in tests:
    r = lambda_handler(t, None)
    print(f"Status: {r['statusCode']}, Body: {r['body']}")

Cost Optimization

Monitor function costs, set appropriate memory, use provisioned concurrency wisely, and clean up old versions.

# cost_optimization.py
# Cost optimization strategies

def estimate_monthly_cost(invocations, avg_duration_ms, memory_mb):
    gb_s = invocations * (avg_duration_ms / 1000) * (memory_mb / 1024)
    compute_cost = max(0, gb_s - 400000) * 0.0000166667
    request_cost = max(0, invocations - 1000000) * 0.0000002
    return compute_cost + request_cost

def optimize_memory(invocations, duration_ms):
    configs = []
    for memory in [128, 256, 512, 1024, 2048, 3008]:
        cost = estimate_monthly_cost(invocations, duration_ms * (512 / memory), memory)
        configs.append((memory, cost))
    
    best = min(configs, key=lambda x: x[1])
    print("Cost optimization for 5M invocations/month, 200ms at 512MB:")
    for mem, cost in configs:
        marker = " <- BEST" if mem == best[0] else ""
        print(f"  {mem}MB -> ${cost:.2f}/month{marker}")

optimize_memory(5000000, 200)

Common Mistakes

  1. Not testing for failure scenarios: Test what happens when DynamoDB throttles, API Gateway times out, or memory is exhausted.

  2. Ignoring function timeouts: Functions that silently timeout cause unpredictable failures. Always set appropriate timeouts.

  3. Not using dead-letter queues: Failed async invocations are lost. Configure DLQs for all production functions.

  4. Over-monitoring without action: Collecting every metric without alerting is noise. Focus on actionable alarms.

  5. Not planning for regional outages: Serverless is regional. Design for multi-region failover for critical applications.

Practice Questions

  1. What is the single responsibility principle for Lambda functions? Each function should perform exactly one business operation. Avoid functions that handle multiple unrelated tasks.

  2. Why initialize connections outside the handler? The execution context is reused for warm invocations. Initializing once saves time on subsequent calls.

  3. How do you optimize serverless costs? Right-size memory, minimize execution time, use provisioned concurrency only when needed, and clean up old versions.

  4. What should you monitor in production serverless? Error rates, duration percentiles, throttles, invocation count, cold start rate, and cost.

  5. Challenge: Review a serverless application against 10 best practices and create a checklist for production readiness review.

FAQ

How many functions should a serverless application have?

One function per business operation. Start with more smaller functions rather than fewer large ones.

Should I use async or sync Lambda invocations?

Use async for fire-and-forget tasks, sync for request-response patterns like API Gateway.

How do I handle Lambda function idempotency?

Use idempotency keys stored in DynamoDB with TTL to detect and skip duplicate invocations.

What testing strategy should I use?

Unit test handler logic, integration test with local emulation, and e2e test against deployed staging.

How do I keep Lambda functions warm?

Use provisioned concurrency for latency-sensitive functions. Scheduled pings are unreliable.

Mini Project

Create a production-ready Lambda function that follows all best practices: connection reuse, structured logging, error handling, input validation, and secret management.

import json
import logging
import os
import re

logger = logging.getLogger()
logger.setLevel(logging.INFO)

class UserService:
    def __init__(self):
        self.db = None
        self.secrets = None
    
    def initialize(self):
        if self.db is None:
            self.secrets = {"db_url": "postgres://..."}
            self.db = {"connected": True}
            logger.info("Initialized database connection")

user_service = UserService()

def validate_email(email):
    return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))

def lambda_handler(event, context):
    user_service.initialize()
    
    try:
        body = json.loads(event.get("body", "{}"))
        logger.info("Processing user creation", extra={"email": body.get("email")})
        
        if not body.get("email") or not validate_email(body["email"]):
            return {"statusCode": 400, "body": json.dumps({"error": "Invalid email"})}
        
        return {"statusCode": 201, "body": json.dumps({"email": body["email"], "status": "created"})}
    
    except Exception as e:
        logger.error("Failed to create user", extra={"error": str(e)})
        return {"statusCode": 500, "body": json.dumps({"error": "Internal error"})}

print(lambda_handler({"body": json.dumps({"email": "alice@example.com"})}, None)["body"])

What's Next

Next: Serverless Python for Python-specific patterns.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro