DEV Community

AlpeshKumbhare
AlpeshKumbhare

Posted on

AWS Serverless Patterns and Anti-Patterns: What Works, What Breaks, and When to Use What

Serverless on AWS isn't "just use Lambda." It's a design philosophy: let AWS manage the infrastructure, pay only for what you use, and build with managed services that scale independently. But the patterns that work in serverless are fundamentally different from traditional architectures — and the anti-patterns are expensive to learn the hard way.

This guide covers the patterns that work in production, the anti-patterns that waste money or cause outages, and the decision framework for when serverless is the right (or wrong) choice.

The Serverless Building Blocks

┌─────────────────────────────────────────────────────────────────────┐
│                    AWS SERVERLESS STACK                               │
├─────────────────────────────────────────────────────────────────────┤
│  COMPUTE        │ Lambda | Fargate (serverless containers)           │
│  API            │ API Gateway (REST/HTTP/WebSocket) | AppSync (GraphQL)│
│  ORCHESTRATION  │ Step Functions | EventBridge Scheduler             │
│  MESSAGING      │ SQS | SNS | EventBridge                          │
│  STORAGE        │ S3 | DynamoDB | Aurora Serverless                  │
│  STREAMING      │ Kinesis | DynamoDB Streams | MSK Serverless        │
│  AUTH           │ Cognito | IAM | Lambda Authorizers                 │
│  OBSERVABILITY  │ CloudWatch | X-Ray | Application Signals           │
└─────────────────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

Key principle: In serverless, you compose applications from managed services. Lambda is the glue between them — not the application itself.


Pattern 1: Synchronous API (Request/Response)

The most common serverless pattern: HTTP API backed by Lambda.

Client → API Gateway → Lambda → DynamoDB / Aurora Serverless
                                    │
                              Response ← ─ ─ ─ ─ ─ ─ ┘
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • API Gateway HTTP API (not REST API) — cheaper, faster, simpler for most cases
  • One Lambda per route (single responsibility) — not a monolith Lambda
  • Keep Lambda warm — use Provisioned Concurrency for latency-sensitive endpoints
  • DynamoDB for simple access patterns — scales with traffic, no connection pooling
  • Aurora Serverless v2 for complex queries — but use RDS Proxy to manage connections

When to Choose HTTP API vs REST API

Feature HTTP API REST API
Cost $1.00/million requests $3.50/million requests
Latency Lower (~10ms added) Higher (~30ms added)
Features JWT auth, CORS, Lambda integration WAF, usage plans, API keys, caching, request validation
Choose when Standard APIs, cost-sensitive Need WAF, throttling plans, request transforms

Pattern 2: Async Event Processing

Events trigger Lambda. Processing happens independently of the caller.

S3 Upload ──→ Lambda: process image ──→ S3: store thumbnail
SQS Message ──→ Lambda: process order ──→ DynamoDB: update status
EventBridge ──→ Lambda: handle event ──→ SNS: send notification
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Always use Dead Letter Queues (DLQ) — failed events go to DLQ, not lost
  • Design for idempotency — events may be delivered more than once
  • Batch processing — SQS Lambda trigger processes up to 10 messages per invocation (cost efficient)
  • Set reserved concurrency — prevent one function from consuming all account concurrency
  • Use event filtering — Lambda event source filtering reduces invocations (cheaper + simpler)

Event Source Filtering Example

{
  "FilterCriteria": {
    "Filters": [
      {
        "Pattern": "{\"body\": {\"status\": [\"critical\"]}}"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Lambda only invokes for messages where body.status == "critical". Other messages are filtered out at the service level (free).


Pattern 3: Workflow Orchestration (Step Functions)

For multi-step processes with branching, retries, and error handling.

Step Function:
├── Validate input
├── Process payment (Lambda)
│     ├── Success → Reserve inventory (Lambda)
│     └── Failure → Notify customer (SNS) → End
├── Ship order (Lambda)
├── Wait 7 days
└── Send follow-up email (Lambda)
Enter fullscreen mode Exit fullscreen mode

Step Functions: Express vs Standard

Feature Standard Express
Duration Up to 1 year Up to 5 minutes
Pricing Per state transition ($0.025/1000) Per execution + duration
Execution model Exactly-once At-least-once
History Full execution history (90 days) CloudWatch Logs only
Use case Long-running workflows, human approval High-volume, short processing (ETL, transforms)

Direct Service Integrations (Skip Lambda)

Step Functions can call 200+ AWS services directly without Lambda:

{
  "Type": "Task",
  "Resource": "arn:aws:states:::dynamodb:putItem",
  "Parameters": {
    "TableName": "Orders",
    "Item": {
      "orderId": {"S.$": "$.orderId"},
      "status": {"S": "confirmed"}
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

No Lambda needed — Step Functions writes to DynamoDB directly. Cheaper, fewer moving parts, lower latency.

Rule: If your Lambda only calls one AWS API — replace it with a direct integration.


Pattern 4: Streaming / Real-Time Processing

For continuous data ingestion and processing.

IoT Devices ──→ Kinesis ──→ Lambda (real-time) ──→ DynamoDB
                  │
                  └──→ Firehose ──→ S3 (data lake)
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Kinesis for ordering and replay — Lambda for real-time processing
  • Firehose for batched delivery — no code needed for S3/Redshift/OpenSearch
  • Tumbling windows — Lambda aggregates over time windows natively
  • Bisect on error — Kinesis + Lambda can split failed batches to isolate the bad record

Pattern 5: Fan-Out / Scatter-Gather

One trigger spawns many parallel processes, results are aggregated.

API → Step Function (Distributed Map):
  ├── Process item 1 (Lambda)
  ├── Process item 2 (Lambda)
  ├── Process item 3 (Lambda)
  └── ... (10,000 concurrent)

  → Aggregate results → Response
Enter fullscreen mode Exit fullscreen mode

Step Functions Distributed Map processes millions of items with up to 10,000 concurrent executions. Use for:

  • Batch processing large datasets from S3
  • Parallel API calls to external services
  • Large-scale data transformation

Pattern 6: GraphQL API (AppSync)

For applications needing flexible, client-driven queries.

Client → AppSync → Resolvers:
  ├── DynamoDB (direct resolver, no Lambda)
  ├── Lambda (complex logic)
  ├── Aurora (SQL queries)
  └── HTTP (external APIs)
Enter fullscreen mode Exit fullscreen mode

AppSync advantages over API Gateway + Lambda:

  • Client fetches exactly what it needs (no over-fetching)
  • Real-time subscriptions (WebSocket) built in
  • Direct DynamoDB/Aurora resolvers (no Lambda needed for CRUD)
  • Caching built in

Pattern 7: Scheduled Tasks

Replace cron servers with serverless scheduling.

EventBridge Scheduler → Lambda: run cleanup
EventBridge Rule (rate/cron) → Lambda: generate report
Step Functions Wait → Lambda: send reminder
Enter fullscreen mode Exit fullscreen mode

EventBridge Scheduler vs EventBridge Rules

Feature Scheduler Rules
One-time events ✅ (at specific time)
Timezone support ✅ (handles DST) ❌ (UTC only)
Scale Millions of schedules Limited rules per bus
Use case Per-entity schedules (user reminders) System-wide recurring jobs

Anti-Patterns: What NOT to Do

Anti-Pattern 1: Lambda Monolith

The mistake: Putting your entire Express/Flask app inside one Lambda function.

❌ BAD: Single Lambda handles ALL routes
   /users, /orders, /products, /admin → one 50MB Lambda

✅ GOOD: One Lambda per route (or per domain)
   /users → users-handler
   /orders → orders-handler
Enter fullscreen mode Exit fullscreen mode

Why it fails: Cold starts scale with package size. One change requires redeploying everything. No independent scaling per endpoint.

Anti-Pattern 2: Lambda Calling Lambda (Synchronous Chain)

The mistake: Lambda A calls Lambda B which calls Lambda C, all synchronously.

❌ BAD:
   Lambda A → invoke → Lambda B → invoke → Lambda C
   (paying for A's time while waiting for B and C)

✅ GOOD:
   Step Functions: A → B → C (orchestrated, not nested)
   Or: A → SQS → B → SQS → C (async, decoupled)
Enter fullscreen mode Exit fullscreen mode

Why it fails: You pay for idle time while waiting. Retry logic becomes complex. Timeouts cascade. Use Step Functions or async messaging instead.

Anti-Pattern 3: Recursive Lambda

The mistake: Lambda invokes itself (or triggers a loop).

❌ DANGEROUS:
   Lambda → writes to S3 → triggers same Lambda → writes to S3 → ...
   (infinite loop = infinite bill)
Enter fullscreen mode Exit fullscreen mode

Fix: Use separate buckets for input/output, or use event source filtering to exclude your own writes.

Anti-Pattern 4: VPC Lambda Without Need

The mistake: Putting Lambda in a VPC "for security" when it doesn't access VPC resources.

Why it fails: VPC Lambda has cold start overhead (ENI creation). If Lambda only calls DynamoDB, S3, or external APIs — it doesn't need VPC. Use VPC only when accessing RDS, ElastiCache, or private EC2 services.

Anti-Pattern 5: Over-Orchestration

The mistake: Using Step Functions for a simple sequential call that could be a direct Lambda + SDK call.

❌ OVER-ENGINEERED:
   Step Function → Lambda (validate) → Lambda (save to DDB)
   (3 resources for what one Lambda could do)

✅ APPROPRIATE:
   Lambda: validate + save to DDB (if it's simple sequential logic)
Enter fullscreen mode Exit fullscreen mode

Rule: Use Step Functions when you need branching, retries, parallel execution, wait states, or error handling across multiple services. Don't use it for simple A→B flows.

Anti-Pattern 6: Ignoring Cold Starts in Latency-Sensitive Paths

The mistake: Using Lambda for a user-facing API with p99 latency SLA of <100ms without addressing cold starts.

Cold start impact:

  • Python/Node.js: 100-300ms
  • Java/.NET: 500-3000ms
  • VPC Lambda: adds 200-500ms on top

Fix:

  • Provisioned Concurrency (pre-warm instances)
  • Lambda SnapStart (Java only — restore from checkpoint)
  • Keep Lambda small (fewer dependencies = faster cold start)
  • Or use Fargate for latency-critical paths with predictable traffic

Anti-Pattern 7: No Concurrency Limits

The mistake: No reserved concurrency on Lambdas in a shared account.

What happens: One function handling a burst consumes all 1,000 concurrent executions → other functions throttled → cascading failures.

Fix: Set reserved concurrency on critical functions. Set account-level concurrency limits on non-critical batch functions.


When Serverless Fits vs When It Doesn't

Serverless Wins

Characteristic Why
Variable/unpredictable traffic Scales to zero, no idle cost
Event-driven workloads Natural fit for trigger-based processing
Rapid prototyping Ship in hours, not days
Per-request cost model preferred Pay only for what you use
Team wants zero infrastructure ops No patching, scaling, or capacity planning

Serverless Doesn't Fit

Characteristic Why Alternative
Consistent high traffic (always on) Fargate/EC2 is cheaper at high utilization Fargate
Execution > 15 minutes Lambda timeout limit ECS tasks / Step Functions
Persistent connections (WebSocket server, gRPC stream) Lambda is request/response Fargate / EC2
Cold start unacceptable (< 50ms p99) Lambda can't guarantee this Fargate / EC2 with ALB
GPU / specialized hardware Lambda doesn't support GPU EC2
Large deployment package (> 250MB) Lambda size limits Containers on Fargate

Cost Optimization Patterns

Pattern Savings
ARM (Graviton) Lambda 20% cheaper, often 10-30% faster
Increase memory (reduce duration) Often cheaper: 256MB × 400ms costs same as 512MB × 180ms
Batch SQS messages (10 per invocation) 10x fewer invocations
Direct integrations (skip Lambda) No Lambda cost for simple pass-through
Event filtering Reduce unnecessary invocations
Provisioned Concurrency (only for latency needs) ⚠️ Adds cost — use only where needed

Summary

Serverless on AWS works when you follow these principles:

  1. Compose from managed services — Lambda is glue, not the application
  2. Single responsibility — one function per task, not monolith Lambdas
  3. Async by default — use SQS/EventBridge between services, not synchronous chains
  4. Step Functions for orchestration — don't build state machines in Lambda code
  5. Direct integrations — if Lambda just calls one AWS API, remove it
  6. Design for failure — DLQs everywhere, idempotent handlers, circuit breakers
  7. Know when it doesn't fit — persistent connections, GPU, >15 min, cold-start-sensitive paths → use Fargate

The best serverless architectures look nothing like traditional three-tier apps shrunk into Lambda. They're composed of events, queues, state machines, and managed services with Lambda connecting the dots.


Alpesh Kumbhare is an AWS Architect at Atos, specializing in AWS serverless architecture and cloud infrastructure automation. Connect on LinkedIn.

Top comments (0)