Verified by Garnet Grid

Serverless Architecture: Patterns & Anti-Patterns

Design serverless systems that scale and don't surprise you with costs. Covers event-driven patterns, cold starts, state management, orchestration, and when serverless is the wrong choice.

Serverless doesn’t mean “no servers” — it means someone else manages the servers. It shines for event-driven, spiky workloads where you pay only for what you use. It fails spectacularly for long-running, steady-state workloads where you end up paying more than containers or VMs. Know the patterns, know the traps, and know when to walk away.

The promise of serverless is compelling: no infrastructure management, automatic scaling from zero to millions of requests, and a pay-per-invocation model that starts at effectively free. The reality is more nuanced — cold starts, execution limits, debugging complexity, and vendor lock-in are real costs that don’t appear on the bill.


Serverless Compute Options

ServiceAWSAzureGCPMax Duration
FunctionsLambdaAzure FunctionsCloud Functions15 min (Lambda)
ContainersFargateContainer AppsCloud RunUnlimited
OrchestrationStep FunctionsDurable FunctionsWorkflows1 year (Step Fn)
API GatewayAPI Gateway / ALBAPI ManagementAPI GatewayN/A
Event busEventBridgeEvent GridEventarcN/A
QueuesSQSQueue StoragePub/SubN/A

Patterns That Work

1. API Backend (Most Common)

The classic serverless pattern: HTTP request → function → database → response.

# AWS Lambda + API Gateway
import json
import boto3

dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('orders')

def handler(event, context):
    method = event['httpMethod']

    if method == 'GET':
        order_id = event['pathParameters']['id']
        result = table.get_item(Key={'order_id': order_id})
        return {
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps(result.get('Item', {}))
        }

    elif method == 'POST':
        body = json.loads(event['body'])
        body['created_at'] = datetime.utcnow().isoformat()
        table.put_item(Item=body)
        return {
            'statusCode': 201,
            'body': json.dumps({'message': 'Created', 'id': body['order_id']})
        }

2. Event Processing Pipeline (Fan-Out)

Decouple processing stages with queues. Each stage scales independently.

S3 Upload → Lambda (Validate) → SQS → Lambda (Process) → DynamoDB
                                  ↓ (on failure)
                            Dead Letter Queue → Lambda (Alert + Log)

EventBridge Rule → Lambda (Enrich) → Kinesis → Lambda (Aggregate) → S3

3. Scheduled Jobs (Cron Replacement)

Replace cron servers with scheduled Lambda functions:

# CloudWatch Events → Lambda (runs every hour)
def handler(event, context):
    stale = find_stale_sessions(hours=24)
    for session in stale:
        cleanup_session(session['id'])

    expired_trials = find_expired_trials()
    for trial in expired_trials:
        downgrade_to_free(trial['user_id'])

    return {'sessions_cleaned': len(stale), 'trials_expired': len(expired_trials)}

4. File Processing (Image/Video/Document)

# S3 trigger → Lambda processes uploaded file
def handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = event['Records'][0]['s3']['object']['key']

    # Download, process, upload result
    image = download_from_s3(bucket, key)
    thumbnail = create_thumbnail(image, size=(300, 300))
    upload_to_s3(bucket, f"thumbnails/{key}", thumbnail)

    # Update metadata in database
    update_image_status(key, status='processed')

Anti-Patterns

1. Monolith-in-a-Lambda

# ❌ Don't cram your entire app into one function
def handler(event, context):
    if event['path'] == '/users': return handle_users(event)
    elif event['path'] == '/orders': return handle_orders(event)
    elif event['path'] == '/payments': return handle_payments(event)
    elif event['path'] == '/reports': return handle_reports(event)
    # 50 more routes...

# Problems:
# - Cold start loads ALL dependencies even for one route
# - Deployment changes affect all routes
# - Cannot scale routes independently
# - Debugging is harder (which route caused the error?)

# ✅ One function per route (or per domain)
# /users → users_handler.py
# /orders → orders_handler.py
# /payments → payments_handler.py

2. Lambda Calling Lambda (Synchronous Chain)

# ❌ Synchronous function chains — brittle, expensive, compounding latency
Lambda A → Lambda B → Lambda C → Lambda D
(A pays for time waiting for B, B pays for time waiting for C...)
Total latency = sum of all functions + cold starts
Total cost = 4x what you'd expect

# ✅ Use Step Functions for orchestration
Step Functions: A → B → C → D
(Built-in retries, error handling, parallel branches, wait states)

# ✅ Or use async events for decoupled processing
Lambda A → SQS → Lambda B → SQS → Lambda C
(Each function runs independently, retries independently)

3. Ignoring Cold Starts

Cold starts happen when a new execution environment is created — the first request is slow.

RuntimeCold Start (avg)Warm InvocationProvisioned
Python 3.12200-500ms1-5ms~0ms (always warm)
Node.js 20100-300ms1-5ms~0ms
Java 211-5 seconds1-10ms~0ms
.NET 8500ms-2s1-10ms~0ms
Rust / Go50-100ms<1ms~0ms

Mitigation Strategies:

StrategyCostEffectivenessUse When
Provisioned concurrency$$$Eliminates cold startsUser-facing, latency-sensitive
Keep-warm pings$Reduces cold startsLow-traffic APIs
Smaller deployment packagesFreeFaster initAlways — remove unused dependencies
SnapStart (Java)Free90% reductionJava on Lambda
Choose lighter runtimesFreeFaster by defaultNew projects (Python, Node, Rust)
# Provisioned concurrency configuration (SAM template)
Resources:
  OrderFunction:
    Type: AWS::Serverless::Function
    Properties:
      Runtime: python3.12
      MemorySize: 512
      AutoPublishAlias: live
      ProvisionedConcurrencyConfig:
        ProvisionedConcurrentExecutions: 10

Cost Analysis

Understanding the cost crossover point is critical for choosing between serverless and containers.

When Serverless Is Cheaper

Low/spiky traffic: < 1M requests/month, 200ms avg execution
- Lambda: ~$3-5/month (compute + requests)
- EC2 t3.small (24/7): ~$15/month
- Fargate (0.25 vCPU): ~$9/month
Winner: Lambda ✅ (especially with scale-to-zero)

When Serverless Is Expensive

Steady high traffic: 100M requests/month, 200ms avg, 512MB memory
- Lambda: ~$400/month (compute) + $60 (requests) = $460/month
- Fargate (2 vCPU, 4GB): ~$120/month
- EC2 reserved (m5.large): ~$50/month
Winner: EC2 or Fargate ✅ (3-9x cheaper at steady load)

Rule of Thumb: If your function runs > 50% of the time at steady load, containers or VMs are cheaper. If traffic is bursty (0-1000 RPS within minutes), serverless wins on both cost and operations.

Hidden Costs

Hidden CostImpactMitigation
Data transfer between services$0.01-0.09/GBMinimize cross-region calls
API Gateway per-request fee$3.50/million requestsUse ALB ($0.40/million) for high volume
CloudWatch Logs ingestion$0.50/GBFilter logs, use sampling
Provisioned concurrency$$ always-on costOnly for latency-critical functions

When NOT to Go Serverless

WorkloadWhy NotBetter Option
WebSocket / long-lived connectionsLambda max 15 min, no persistent connectionsFargate, EC2, Cloud Run
Steady-state high throughputCost inefficient at sustained loadContainers (Fargate, ECS, K8s)
GPU / ML inferenceNo GPU support in LambdaSageMaker, GPU instances, Cloud Run GPU
Legacy apps requiring local filesystemStateless functions, /tmp limited to 512MB-10GBContainers, VMs
Sub-10ms latency requiredCold starts unpredictableContainers with health checks
Complex multi-step transactionsDifficult to manage distributed stateContainers with SAGA pattern

Serverless Observability

Serverless makes debugging harder because there are no servers to SSH into. Invest in observability from day one.

LayerToolWhy
Distributed tracesAWS X-Ray, Datadog, HoneycombSee request flow across functions
Structured loggingCloudWatch Logs Insights, DatadogQuery logs without grep
Custom metricsCloudWatch EMF, DatadogBusiness-specific measurements
Error trackingSentry, LumigoStack traces with context

Serverless Cost Traps

TrapHow It HappensPrevention
Recursive invocationsLambda triggers itself via S3 or SNS loopDead letter queues, invocation limits
Cold start cascadingDownstream Lambdas cold-start in sequenceProvisioned concurrency for critical paths
Excessive loggingCloudWatch costs exceed compute costsStructured logging, sampling, log levels
Over-provisioned memorySetting 1GB when 256MB sufficesMemory profiling with AWS Lambda Power Tuning
Idle DynamoDB capacityProvisioned throughput for peak, idle 23 hours per daySwitch to on-demand for variable workloads
Unnecessary API GatewayUsing API Gateway when ALB + Lambda sufficesUse ALB for internal APIs (10x cheaper)

When NOT to Go Serverless

  • Long-running processes (over 15 minutes) — Use ECS or Fargate or Step Functions instead
  • Stateful applications — WebSocket servers, game servers, real-time collaboration
  • GPU workloads — ML inference, video processing (use ECS with GPU instances)
  • High-throughput streaming — Sustained 100K+ events per sec (consider Kinesis + EC2)
  • Predictable, constant load — If traffic is steady 24/7, reserved EC2 is cheaper

Checklist

  • Workload pattern analyzed (spiky vs steady) — serverless appropriate?
  • Cold start impact assessed for user-facing functions
  • Cost modeling done at projected scale with crossover analysis
  • Event-driven architecture designed (not synchronous chains)
  • Dead letter queues configured for every async trigger
  • Observability set up (distributed tracing + structured logging)
  • Function deployment packages minimized (remove unused deps)
  • Timeout and memory configured per function (not defaults)
  • API Gateway throttling and usage plans configured
  • Provisioned concurrency set for latency-critical paths
  • Monitoring alerts on error rate, duration, and throttling

:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For cloud architecture consulting, visit garnetgrid.com. :::

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →