Handling Traffic Spikes with SQS, SNS, and Lambda: A Resilient Order-Processing Pattern
Hypothetical scenario, created for learning purposes.
Handling Traffic Spikes with SQS, SNS, and Lambda: A Resilient Order-Processing Pattern
Hypothetical scenario, created for learning purposes.
The problem
Imagine Trilha Sports, a fictional online shop that runs a flash sale every Friday. For ten minutes, order volume jumps from a few per second to hundreds. The current setup calls the inventory, billing, and email services one after another inside a single API. When one of them slows down, orders time out and customers see errors.
The goal is to decouple these steps so a slow service never blocks the checkout.
Architecture
- Amazon API Gateway: receives the order and returns quickly.
- Amazon SNS: publishes an
OrderCreatedevent (fan-out). - Amazon SQS: one queue per consumer (inventory, billing, email), each with its own dead-letter queue (DLQ).
- AWS Lambda: one function per queue, processing messages in batches.
- Amazon CloudWatch: alarms on queue depth and DLQ size.
Each consumer scales and fails independently. If billing slows down, its queue grows, but inventory and email keep working.
Step by step
1. Publish one event, deliver to many
The API function only validates the order, saves it, and publishes an event:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import json
import boto3
sns = boto3.client("sns")
TOPIC_ARN = "arn:aws:sns:us-east-1:123456789012:order-created"
def handler(event, context):
order = json.loads(event["body"])
# validate and persist the order here
sns.publish(
TopicArn=TOPIC_ARN,
Message=json.dumps(order),
MessageAttributes={
"eventType": {"DataType": "String", "StringValue": "OrderCreated"}
},
)
return {"statusCode": 202, "body": json.dumps({"status": "accepted"})}Returning
202 Accepted tells the client the order was received, not that every step has finished.2. Give each consumer its own queue
Subscribe each SQS queue to the SNS topic. Every queue gets a copy of the event, and each one is configured with:
- a DLQ and a
maxReceiveCount(for example, 3), so a message that keeps failing is isolated instead of retried forever; - a visibility timeout at least six times the Lambda timeout, as AWS recommends, to avoid duplicate processing.
3. Process in batches, fail individually
Enable partial batch responses on the Lambda event source mapping (
ReportBatchItemFailures). Then a single bad message doesn't force the whole batch to be retried:1
2
3
4
5
6
7
8
9
10
11
import json
def handler(event, context):
failures = []
for record in event["Records"]:
try:
body = json.loads(record["body"])
process_order(body) # your business logic
except Exception:
failures.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failures}4. Make processing idempotent
SQS standard queues deliver messages at least once, so a message can arrive twice. Use the order ID as an idempotency key, for example with a conditional write in DynamoDB, so processing the same order twice has no extra effect.
Lessons learned
- Design for duplicates. At-least-once delivery is a feature of the system, not a bug. Idempotency is your responsibility.
- Always configure a DLQ. Without one, a poison message can block progress or retry indefinitely.
- Alarm on the DLQ, not just the main queue. A non-empty DLQ means something needs human attention.
- Control concurrency. Set a maximum concurrency on the event source mapping so a spike doesn't overwhelm a downstream database.
- Keep messages small. Send an ID and fetch details when needed, rather than moving large payloads through the queue.
Next steps
- Amazon EventBridge for richer routing rules between services;
- AWS Step Functions when a workflow needs ordering, compensation, or human approval;
- AWS X-Ray for tracing a request across SNS, SQS, and Lambda;
- FIFO queues if strict ordering per customer matters, accepting the throughput trade-offs.
Conclusion
Decoupling with SNS and SQS turns a fragile chain of synchronous calls into independent, observable steps. The pattern isn't complicated, but it only works well when you plan for retries, duplicates, and failures from the start.
How do you handle retries and failed messages in your event-driven systems? Share your approach in the comments.
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article