Serverless Architecture Patterns
Serverless means you write code and let AWS run it for you, with no servers to patch or scale by hand. You pay only for what you use, and AWS handles capacity automatically. This page walks through the canonical serverless patterns you will reach for again and again, and the traps that catch teams once traffic grows. The big shift to understand up front: serverless does not remove complexity, it moves it from managing servers to wiring events and respecting service limits.
The main building block is AWS Lambda (a service that runs your function code on demand, with no server to manage). Around it you compose other managed services to form a complete system.
API backend: API Gateway + Lambda + DynamoDB
The most common serverless pattern is a web or mobile backend. A request hits Amazon API Gateway (a fully managed front door for HTTP APIs), which invokes a Lambda function, which reads or writes Amazon DynamoDB (a fully managed NoSQL key-value database that scales automatically).
When to use this: bursty or unpredictable traffic, APIs that should cost near nothing when idle, and small teams that do not want to run servers. When NOT to: steady high-volume traffic (a 24/7 busy API on always-on containers can be cheaper), or workloads needing long-running connections.
Create the function from the CLI:
aws lambda create-function \
--function-name get-order \
--runtime nodejs22.x \
--role arn:aws:iam::123456789012:role/lambda-dynamo-role \
--handler index.handler \
--zip-file fileb://function.zip
Output:
{
"FunctionName": "get-order",
"FunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:get-order",
"Runtime": "nodejs22.x",
"State": "Active",
"PackageType": "Zip"
}
Console steps:
- Open the Lambda console and choose Create function > Author from scratch.
- Name it
get-order, pick the Node.js 22.x runtime, attach an execution role with DynamoDB read access, and click Create function. - Open the API Gateway console, create an HTTP API, add a route
GET /orders/{id}, and set the integration target to theget-orderLambda. - Deploy the API. API Gateway gives you an invoke URL like
https://abc123.execute-api.us-east-1.amazonaws.com.
Cost note: Lambda’s free tier covers 1M requests and 400,000 GB-seconds per month. DynamoDB on-demand bills per request. A low-traffic API can run for cents per month, but a runaway loop calling Lambda can ring up real money fast — set a budget alarm.
Event-driven processing: S3 triggers
A second pattern processes data as it arrives. Upload a file to Amazon S3 (Simple Storage Service, AWS object storage), and S3 emits an event that triggers a Lambda — for example to make a thumbnail or parse a CSV.
When to use this: image/video processing, log parsing, ingest pipelines. When NOT to: ordered processing where event order matters (S3 events do not guarantee order).
aws s3api put-bucket-notification-configuration \
--bucket uploads-devcraftly \
--notification-configuration '{
"LambdaFunctionConfigurations": [{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:123456789012:function:make-thumbnail",
"Events": ["s3:ObjectCreated:*"]
}]
}'
In the console: open the S3 bucket > Properties > Event notifications > Create event notification, select All object create events, and choose your Lambda as the destination.
Fan-out: SNS and SQS
When one event must reach several independent consumers, use fan-out. Amazon SNS (Simple Notification Service, a pub/sub messaging service) publishes a message to many subscribers at once. Often each subscriber is an Amazon SQS (Simple Queue Service, a managed message queue) queue feeding its own Lambda. The queue absorbs spikes and lets each consumer process at its own pace.
When to use this: one event, many reactions (order placed → email service + analytics + inventory). When NOT to: a single consumer (just trigger Lambda directly).
aws sns publish \
--topic-arn arn:aws:sns:us-east-1:123456789012:order-events \
--message '{"orderId":"A-1009","status":"PLACED"}'
Output:
{
"MessageId": "9f6a1b2c-3d4e-5f60-7a8b-9c0d1e2f3a4b"
}
The SNS-to-SQS layer matters because SQS gives you a buffer and a retry/dead-letter mechanism, so a slow or failing consumer does not lose messages or back-pressure the publisher.
Orchestration: Step Functions
For multi-step workflows — call A, then B, then branch, then retry on failure — wiring Lambdas to each other by hand becomes a tangle. AWS Step Functions (a managed workflow service) lets you describe the steps as a state machine in JSON, and it handles retries, branching, and waiting.
When to use this: order fulfillment, ETL pipelines, anything with steps, retries, or human approval. When NOT to: a single quick function (the orchestration overhead is not worth it).
{
"Comment": "Order workflow",
"StartAt": "ChargeCard",
"States": {
"ChargeCard": { "Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:charge",
"Retry": [{ "ErrorEquals": ["States.TaskFailed"], "MaxAttempts": 3 }],
"Next": "ShipOrder" },
"ShipOrder": { "Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:ship",
"End": true }
}
}
Pattern comparison
| Pattern | Core services | Best for | Avoid when |
|---|---|---|---|
| API backend | API Gateway + Lambda + DynamoDB | Bursty REST/HTTP APIs | Steady heavy traffic |
| Event processing | S3 + Lambda | File/data ingest on arrival | Strict ordering needed |
| Fan-out | SNS + SQS + Lambda | One event, many consumers | A single consumer |
| Orchestration | Step Functions + Lambda | Multi-step workflows | One quick task |
The limits that bite
Serverless feels infinitely scalable, but it is not. These are the traps:
- Lambda concurrency limits. Each AWS account has a default of 1,000 concurrent Lambda executions per Region. A traffic spike that exceeds it gets throttled, and that throttling can cascade — failed invocations retry, retries add load, and the whole system stalls. Use reserved concurrency to protect critical functions and provisioned concurrency to avoid cold-start latency.
- Downstream services that do not scale like Lambda. Lambda can launch thousands of functions in seconds. If each one opens a connection to a non-serverless database (like a single Amazon RDS instance), you can exhaust its connection pool and crash it. Put Amazon RDS Proxy in front of relational databases, or use DynamoDB which scales with you.
- Idempotency under at-least-once delivery. SQS, SNS, and EventBridge deliver a message at least once, which means sometimes twice. Your function must be idempotent — processing the same message twice must not double-charge a card. Use a unique message ID and record processed IDs in DynamoDB before acting.
Gotcha: The hardest serverless bugs live in the event wiring, not the code. A missing permission, a wrong event filter, or a duplicate delivery is far more common than a logic error. Test failure paths, not just the happy path.
Best practices
- Set reserved concurrency on important functions so one noisy workload cannot starve the rest.
- Front relational databases with RDS Proxy; prefer DynamoDB for naturally serverless data access.
- Make every consumer idempotent and add a dead-letter queue to catch messages that repeatedly fail.
- Keep functions small and single-purpose; orchestrate with Step Functions instead of chaining Lambdas directly.
- Add CloudWatch alarms on throttles, errors, and queue depth, plus an AWS Budgets alert so a runaway loop cannot surprise you.
- Use infrastructure as code (AWS SAM, CloudFormation, or Terraform) so event wiring is reviewable and repeatable.