Skip to content

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.

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.
flowchart TD A[Client application] --> B[SNS topic<br/>order events] B -->|order.created| C[SQS queue<br/>OrderProcessing] B -->|order.created and order.cancelled| D[SQS queue<br/>Email] B -->|order.cancelled| E[SQS queue<br/>OrderCancellation] C --> F[OrderProcessing service] D --> G[Email service] E --> H[OrderCancellation service] style B fill:#f96,stroke:#333 style C fill:#9cf,stroke:#333 style D fill:#9cf,stroke:#333 style E fill:#9cf,stroke:#333

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.

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:

Terminal window
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'],
}),
),
},
}));

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.

  • 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.