SNS fan-out with per-subscriber message filtering
A common integration requirement: when something happens, two or more services must each react, independently and at the same time. An order is placed and both the email service and the order-processing service need to know; an order is cancelled and both the email service and the cancellation service need to know.
The pattern
Section titled “The pattern”Publish to an SNS topic and subscribe an SQS queue per consumer, with a subscription filter policy on each queue so it receives only the event types it handles.
- SNS delivers each published message to every subscriber, so consumers act in parallel rather than in a chain.
- An SQS queue in front of each consumer buffers the message. A consumer that is down or slow does not lose events and does not hold up the others.
- The filter policy keeps each queue clean, so a consumer is not paying to receive and discard messages it has no interest in.
Filter policies
Section titled “Filter policies”A filter policy is a JSON object whose properties name the fields to match and the values to accept. There is no wrapper key — the policy is the inner object:
// OrderProcessing queue{ "type": ["com.marketplace.order.created"] }
// OrderCancellation queue{ "type": ["com.marketplace.order.cancelled"] }
// Email queue — both event types{ "type": ["com.marketplace.order.created", "com.marketplace.order.cancelled"] }The policy is attached to the subscription, not to the topic or the queue, which is what makes each subscriber’s view independent.
The trap: filter policy scope
Section titled “The trap: filter policy scope”A filter policy is evaluated against message attributes by default. If the field you are filtering on lives in the message body — which it does whenever you publish a structured envelope such as CloudEvents — you must say so, or the subscription silently matches nothing and the queue stays empty:
{ "specversion": "1.0", "type": "com.marketplace.order.created", "source": "/marketplace/orders", "id": "order-123-456", "time": "2026-03-10T10:30:00Z", "datacontenttype": "application/json", "data": { "orderId": "123456", "customerId": "789", "items": ["item1", "item2"], "total": 99.99 }}Here type is in the body. Set FilterPolicyScope to MessageBody:
aws sns subscribe \ --topic-arn arn:aws:sns:region:account-id:topic-name \ --protocol sqs \ --notification-endpoint arn:aws:sqs:region:account-id:queue-name \ --attributes '{ "FilterPolicyScope": "MessageBody", "FilterPolicy": "{\"type\":[\"com.marketplace.order.created\"]}" }'The alternative is to publish type as a message attribute as well as in the body, and
leave the scope at its default. Either is fine; what is not fine is filtering on a body field
while leaving the scope at MessageAttributes, because nothing errors — the subscription is
created, the policy is valid, and no message ever matches.
In the AWS CDK, SqsSubscription exposes both forms: filterPolicy for message attributes and
filterPolicyWithMessageBody for the body.
const queue = new sqs.Queue(this, 'OrderProcessingQueue');const topic = new sns.Topic(this, 'OrdersTopic');
topic.addSubscription(new subs.SqsSubscription(queue, { filterPolicyWithMessageBody: { type: sns.FilterOrPolicy.filter( sns.SubscriptionFilter.stringFilter({ allowlist: ['com.marketplace.order.created'], }), ), },}));When to use EventBridge instead
Section titled “When to use EventBridge instead”SNS fan-out with filtering is the right tool when the routing is simple, the consumer set is known, and throughput is high. EventBridge is the better tool once any of the following apply:
- Routing depends on more than a field match — content-based rules, prefix or numeric matching, or rules over nested structures.
- You want the events archived and replayable, which SNS does not offer.
- You want schema discovery and validation.
- The targets are AWS services rather than queues; EventBridge has by far the wider set of native targets.
- Events originate from AWS services or SaaS partners, which reach EventBridge directly.
EventBridge costs more per event than SNS, and its per-Region PutEvents quotas are lower than
SNS’s throughput. That is usually the deciding trade — see the
integration pattern comparison matrix
for the current figures.
Variations worth knowing
Section titled “Variations worth knowing”- API Gateway → SQS → Lambda, skipping the topic entirely, is simpler and cheaper when there is only one consumer and there will only ever be one.
- A router function in front of several topics gives finer control than filter policies at the cost of code you now own. Reach for EventBridge before writing one.
- A dead-letter queue on every subscription is not a variation; it is the default you should be applying, in both this pattern and its EventBridge equivalent.