Most "event-driven" systems drift into distributed pain because two primitives aren't respected: backpressure and idempotency. Here is how we combine Lambda and SQS into architectures that survive production.
The reference pattern
At the heart of every event-driven system we ship:
Producer → SNS/EventBridge → SQS → Lambda → Downstream
↘ DLQ
Each arrow is a contract. Each component has one job. Let's walk through why.
Why SNS or EventBridge in front
SQS is point-to-point: one queue, one logical consumer. If two services need the same event (audit log and email notifier), you don't want producers duplicating the publish. Put SNS or EventBridge in front: both fan events to multiple SQS queues, with per-subscriber filtering.
- SNS: simpler, cheaper at high volume, topic-based.
- EventBridge: richer routing (content-based filtering, schema registry, cross-account delivery). Slightly more expensive.
Why SQS in the middle
Even though Lambda has direct SNS and EventBridge integrations, we almost always put SQS between them for production workloads. Reasons:
- Backpressure. If the consumer slows down, messages accumulate in SQS. No backpressure signal on direct integration: Lambda is throttled and SNS/EventBridge retries aggressively.
- Replay. A Lambda failure in a direct integration routes to a destination, but once processed, gone. An SQS DLQ gives you a replayable archive.
- Batching. Lambda reads from SQS in batches of up to 10,000 messages on Standard queues (with a batching window; 6 MB payload cap), or 10 on FIFO queues. Far more efficient per invocation.
- Decoupling deploys. Roll out a consumer change with zero producer impact.
Idempotency is the first rule
SQS delivers at-least-once. SNS and EventBridge do the same. That means every consumer will occasionally see the same message twice. If your handler is not idempotent, your system has a data bug waiting.
Strategies we use:
- Conditional writes: DynamoDB
PutItemwithattribute_not_exists(pk)orConditionExpression. - Idempotency key tables: Powertools' idempotency utility stores the event's ID and the result in DynamoDB.
- Natural idempotency: design events so replaying them is safe (e.g. "set status to X" rather than "increment count").
// Powertools idempotency in a handler (TypeScript)
import { makeHandlerIdempotent } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
const persistence = new DynamoDBPersistenceLayer({ tableName: 'IdempotencyStore' });
export const handler = makeHandlerIdempotent(async (event) => {
// guaranteed to run once per event, even on SQS redelivery
await processOrder(event);
}, { persistenceStore: persistence });
Partial batch response
By default, if one message in a batch fails, all ten are retried, including the nine that succeeded. Always enable ReportBatchItemFailures and return the failed message IDs:
export const handler = async (event) => {
const batchItemFailures = [];
for (const record of event.Records) {
try {
await process(record);
} catch (e) {
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
DLQ + replay are a feature, not a fallback
Treat the DLQ as part of your normal operations loop:
- Alarm on DLQ depth > 0.
- Ship a small replay tool that drains the DLQ back to the main queue after a fix, pre-built so you're not writing it at 3 AM.
- Tag DLQ messages with the error and attempt count so you can group and triage quickly.
Observability: the minimum viable set
The events you must be able to see:
- Producer emit rate, per event type.
- Queue depth and age of oldest message: your lagging indicator.
- Consumer invocation count, duration, error rate.
- DLQ depth: the canary.
- End-to-end latency: time from event emitted to downstream write.
A system you can't see is a system you can't operate. Observability is not a phase 2; it ships with phase 1.
Failure modes and how to design around them
- Poison messages. DLQ + max receive count. Replay after fix.
- Downstream outages. SQS absorbs. Monitor queue age; that's your alert.
- Consumer bugs. Deploy the consumer, messages pile up harmlessly, drain after fix.
- Runaway producers. Reserved concurrency on the consumer caps blast radius.
- Duplicate processing. Idempotency everywhere, always.
An event pipeline losing messages? Describe your architecture: a one-page diagnosis within 48 hours.
Get my diagnosis →The short list
- SNS or EventBridge → SQS → Lambda. Default pattern.
- Always idempotent consumers.
- Partial batch response is table stakes.
- DLQ + replay tool, pre-built.
- Alarm on queue age and DLQ depth, not invocation error rate.
- Observe end-to-end latency, not just component latency.