SQS vs SNS vs EventBridge: Choosing AWS Messaging
AWS has three managed services that all look like "send a message from A to B": Amazon SQS, Amazon SNS and Amazon EventBridge. They overlap enough to cause confusion and differ enough that picking the wrong one leads to awkward workarounds later.
The short version: SQS is a queue for buffering work, SNS is a pub/sub topic for fanning out notifications, and EventBridge is an event bus for routing events by content. This post explains each one, compares them directly, and shows the combinations that work well in production.
The mental model
Think about the relationship between producer and consumer:
- Queue (SQS): one logical consumer pulls work at its own pace. The producer does not care who processes the message, only that it gets processed eventually.
- Topic (SNS): the producer pushes one message and every subscriber receives a copy immediately. Subscribers are known endpoints such as queues, Lambda functions, HTTP endpoints, email or SMS.
- Event bus (EventBridge): the producer publishes a fact ("OrderPlaced") without knowing who cares. Rules match events on their content and route them to targets.
Queues decouple in time, topics decouple one-to-many delivery, and buses decouple producers from the routing logic itself.
Amazon SQS
SQS stores messages until a consumer receives and deletes them. A received message becomes invisible for the visibility timeout; if the consumer does not delete it in time, it reappears and can be processed again.
Standard and FIFO queues
Standard queues provide at-least-once delivery and best-effort ordering, with very high throughput. FIFO queues guarantee ordering within a message group and deduplicate messages sent with the same deduplication ID within a five-minute window. FIFO throughput is lower than standard, so use it only when order genuinely matters, and design message group IDs (for example, per customer or per aggregate) so work can still run in parallel across groups.
Failure handling
Configure a dead-letter queue (DLQ) with a maxReceiveCount. After that many failed receives, SQS moves the message to the DLQ where it can be inspected and redriven. Set the visibility timeout comfortably above your processing time; for Lambda consumers, AWS recommends at least six times the function timeout.
aws sqs send-message \
--queue-url https://sqs.ap-southeast-1.amazonaws.com/123456789012/orders \
--message-body '{"orderId":"o-1001","total":250}'
aws sqs receive-message \
--queue-url https://sqs.ap-southeast-1.amazonaws.com/123456789012/orders \
--max-number-of-messages 10 \
--wait-time-seconds 20
--wait-time-seconds enables long polling, which reduces empty responses and cost when you poll SQS yourself. Lambda event source mappings handle polling for you.
Amazon SNS
SNS pushes each published message to all subscriptions on a topic. It does not store messages for later retrieval: if a subscriber is unavailable, SNS retries according to its delivery policy and can send undeliverable messages to a subscription-level DLQ.
The most important SNS feature for architecture is the subscription filter policy. Each subscription can filter on message attributes or, with the filter policy scope set to MessageBody, on the JSON payload itself. This lets several consumers share one topic while each receives only what it needs.
aws sns publish \
--topic-arn arn:aws:sns:ap-southeast-1:123456789012:orders \
--message '{"orderId":"o-1001","total":250}' \
--message-attributes '{"eventType":{"DataType":"String","StringValue":"OrderPlaced"}}'
SNS also offers FIFO topics, which deliver to SQS queues and preserve ordering per message group across the fan-out.
Amazon EventBridge
EventBridge receives events on an event bus and evaluates them against rules. Each rule has an event pattern and up to several targets, which can be Lambda, SQS, SNS, Step Functions, API destinations (arbitrary HTTP APIs), another event bus, and many other AWS services.
Beyond routing, EventBridge brings features the other two do not:
- Content-based patterns with prefix, suffix, numeric ranges,
anything-butandexistsmatching. - Input transformation to reshape an event before it reaches a target.
- Archive and replay to store events and re-send them later, useful for rebuilding a projection or testing a new consumer.
- Schema registry and discovery to document event shapes.
- AWS service events on the default bus, for example EC2 state changes or S3 object events, without writing any glue code.
- Pipes for point-to-point source-to-target integration and Scheduler for one-off and recurring schedules.
Events have a standard envelope with source, detail-type and detail:
import { EventBridgeClient, PutEventsCommand } from '@aws-sdk/client-eventbridge';
const client = new EventBridgeClient({});
export async function publishOrderPlaced(order: { orderId: string; total: number }) {
const res = await client.send(new PutEventsCommand({
Entries: [{
EventBusName: 'orders-bus',
Source: 'app.orders',
DetailType: 'OrderPlaced',
Detail: JSON.stringify({ ...order, version: 1 }),
}],
}));
// PutEvents can partially fail; it does not throw for individual entries.
if (res.FailedEntryCount && res.FailedEntryCount > 0) {
const failed = res.Entries?.filter((e) => e.ErrorCode);
throw new Error(`EventBridge rejected entries: ${JSON.stringify(failed)}`);
}
}
Checking FailedEntryCount is easy to forget and is one of the most common EventBridge bugs. The same applies to SendMessageBatch in SQS and PublishBatch in SNS.
Side-by-side comparison
| Aspect | SQS | SNS | EventBridge |
|---|---|---|---|
| Pattern | Queue, point-to-point | Pub/sub fan-out | Event bus, content routing |
| Delivery model | Consumer pulls | Service pushes | Service pushes |
| Persistence | Retains until deleted or retention period ends | No storage for later reads | Optional archive and replay |
| Filtering | None (consumer decides) | Filter policies on attributes or body | Rich event patterns |
| Ordering | FIFO queues | FIFO topics | No ordering guarantee |
| Consumer backpressure | Built in | None (subscriber must keep up) | None (target must keep up) |
| Third-party SaaS events | No | No | Yes, via partner event sources |
Check the current quotas and pricing pages for throughput and payload size, because they differ between services and change over time.
Patterns that work well
Fan-out with SNS and SQS
Publish once to SNS, subscribe one SQS queue per consumer service. Each service gets its own buffer, its own DLQ and its own scaling, and a slow consumer cannot affect the others.
import * as sns from 'aws-cdk-lib/aws-sns';
import * as sqs from 'aws-cdk-lib/aws-sqs';
import * as subs from 'aws-cdk-lib/aws-sns-subscriptions';
import { Duration } from 'aws-cdk-lib';
const topic = new sns.Topic(this, 'OrdersTopic');
const billingDlq = new sqs.Queue(this, 'BillingDlq', { retentionPeriod: Duration.days(14) });
const billingQueue = new sqs.Queue(this, 'BillingQueue', {
visibilityTimeout: Duration.seconds(180),
deadLetterQueue: { queue: billingDlq, maxReceiveCount: 5 },
});
topic.addSubscription(new subs.SqsSubscription(billingQueue, {
rawMessageDelivery: true,
filterPolicy: {
eventType: sns.SubscriptionFilter.stringFilter({ allowlist: ['OrderPlaced', 'OrderRefunded'] }),
},
}));
rawMessageDelivery delivers the original payload instead of wrapping it in the SNS JSON envelope, which keeps consumer code simpler.
EventBridge into SQS
For domain events shared across teams, publish to a custom EventBridge bus and have each consumer own a rule that targets its own SQS queue. The producer never changes when a new consumer appears, and the queue gives the consumer backpressure and retries.
import * as events from 'aws-cdk-lib/aws-events';
import * as targets from 'aws-cdk-lib/aws-events-targets';
const bus = new events.EventBus(this, 'OrdersBus', { eventBusName: 'orders-bus' });
new events.Rule(this, 'HighValueOrders', {
eventBus: bus,
eventPattern: {
source: ['app.orders'],
detailType: ['OrderPlaced'],
detail: { total: [{ numeric: ['>=', 1000] }] },
},
targets: [new targets.SqsQueue(fraudReviewQueue, { deadLetterQueue: ruleDlq })],
});
bus.archive('OrdersArchive', {
eventPattern: { source: ['app.orders'] },
retention: Duration.days(30),
});
The deadLetterQueue on the target catches events EventBridge could not deliver to the queue at all, which is separate from the queue's own DLQ for processing failures.
Consuming SQS from Lambda safely
When Lambda polls SQS in batches, one failed message should not force the whole batch to be retried. Enable partial batch responses and return only the failed message IDs:
import type { SQSEvent, SQSBatchResponse } from 'aws-lambda';
export const handler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const batchItemFailures: SQSBatchResponse['batchItemFailures'] = [];
for (const record of event.Records) {
try {
const order = JSON.parse(record.body);
await processOrder(order); // must be idempotent
} catch (err) {
console.error('Failed to process', record.messageId, err);
batchItemFailures.push({ itemIdentifier: record.messageId });
}
}
return { batchItemFailures };
};
In CDK, wire it with new SqsEventSource(queue, { batchSize: 10, reportBatchItemFailures: true }) from aws-cdk-lib/aws-lambda-event-sources. Without reportBatchItemFailures, the returned list is ignored.
Pitfalls to design for
- Duplicates are normal. Standard SQS, SNS and EventBridge all deliver at least once. Make handlers idempotent, for example with a conditional write on an idempotency key in DynamoDB.
- Ordering is rarely global. If you need order, scope it to an entity with FIFO message groups, or include a version number and ignore stale events.
- Poison messages. Always configure DLQs and alarm on
ApproximateNumberOfMessagesVisiblefor them. A DLQ nobody watches is just a slower way to lose data. - Payload size. Keep events small and put large documents in S3, sending a reference instead.
- Event contracts. Version your
detailschema and treat event shapes as a public API between teams.
How to choose
- Background job or work that must be smoothed out under load: SQS.
- The same message must reach several known consumers quickly, possibly including email, SMS or mobile push: SNS, usually with SQS subscribers.
- Domain events consumed by teams you do not control, content-based routing, SaaS or AWS service events, or a need to replay history: EventBridge, with SQS in front of consumers that need buffering.
Conclusion
SQS, SNS and EventBridge are complementary rather than competing. SQS gives consumers control over pace and retries, SNS gives fast fan-out to known subscribers, and EventBridge gives loosely coupled, content-based routing with replay. Most mature systems use all three: EventBridge or SNS to distribute, SQS to buffer, and Lambda or containers to process. Whichever you choose, assume at-least-once delivery, make consumers idempotent, and watch your dead-letter queues.