AWS Integration

Amazon SNS Hands-On: Topics, Fan-Out, Message Filtering & Subscriptions

An order gets placed on your e-commerce site, and five things need to happen: the warehouse system needs to reserve stock, the billing service needs to charge the card, the notification service needs to email the buyer, the analytics pipeline needs to record the event, and the fraud team’s function needs to score it. The naive design has your checkout code call all five services in a loop — which means checkout is now coupled to five systems, slows to the speed of the slowest one, and breaks entirely when the analytics endpoint is down. Amazon SNS (Simple Notification Service) exists to break exactly that coupling. Your checkout code publishes one message to a topic and moves on; SNS pushes a copy to every interested subscriber. Checkout never knows — and never needs to know — who is listening.

That pattern is publish/subscribe (pub/sub), and SNS is AWS’s managed, serverless implementation of it. A publisher sends a message to a topic (a named channel); zero or more subscribers attach to that topic through a subscription, each naming a protocol — an SQS queue, a Lambda function, an HTTP/S webhook, an email address, an SMS number, a mobile push endpoint, or a Kinesis Data Firehose stream. When you publish once, SNS fans out the message to all of them in parallel, in milliseconds, with no code on your side to manage the list. Add a subscriber next quarter and no publisher changes. That decoupling — one-to-many, push-based, publisher-blind — is the entire value proposition, and it is the backbone of most event-driven architectures on AWS.

By the end of this article you will have built the real thing with your own hands: a topic that fans out to two SQS queues with different filter policies (so each queue receives only the events it asked for) plus a Lambda function (that receives everything), with a subscription dead-letter queue catching anything SNS can’t deliver — first with the aws CLI, then the identical stack as Terraform, then torn down. Along the way you will learn why fronting Lambda with SQS beats subscribing Lambda directly, when you actually need a FIFO topic, how filter policies and raw message delivery change what a subscriber sees, how the topic policy and KMS encryption gate cross-account publishing, and — critically — a symptom-to-fix troubleshooting playbook for the dozen ways a fan-out silently drops messages. Read the prose once; keep the tables open when your own topic misbehaves.

What problem this solves

Without a pub/sub broker, every producer must know every consumer. That is fine with one consumer and unbearable with five: the producer holds a hard-coded list of endpoints, retries each one, handles each one’s outages, and redeploys every time the list changes. Worse, the producer’s latency becomes the sum of all downstream calls, and a single slow or failing consumer can stall or crash the producer. This is temporal coupling (everyone must be up at once) layered on implementation coupling (the producer knows everyone’s address), and it is the reason monoliths are so hard to split.

SNS deletes both couplings at once. The publisher’s only dependency is the topic ARN; it publishes and returns in a few milliseconds regardless of how many subscribers exist or whether they are healthy. SNS owns the fan-out, the parallel delivery, the retries, and the durability. A subscriber that is down does not affect the publisher or the other subscribers — SNS retries it and, if you configured one, drops the undeliverable message into a DLQ for later replay rather than losing it. New subscribers self-serve: they subscribe to the topic (subject to the topic policy) without touching the producer.

Who hits the pain this solves: anyone wiring more than one consumer to an event, anyone splitting a monolith into services, anyone building “when X happens, do A and B and C” workflows. And SNS bites beginners in a very specific set of places — the SQS queue that stays empty because its queue policy doesn’t allow the topic; the subscriber that receives nothing because its filter policy is too strict; the email that never arrives because the subscription is unconfirmed; the consumer that chokes because it’s parsing the SNS envelope instead of turning on raw delivery; the events that vanish under an outage because no DLQ was attached. Get those five right and fan-out is boringly reliable. Here is the whole field on one screen — every piece you will meet, what it is, and the classic trap attached to it:

Piece What it is You configure it as The beginner trap
Topic A named pub/sub channel create-topic (Standard or FIFO) Using FIFO “to be safe” and capping throughput
Subscription A binding of one endpoint to a topic subscribe (protocol + endpoint) Left in PendingConfirmation, silently receiving nothing
Protocol How SNS delivers (SQS, Lambda, HTTP…) Chosen at subscribe time Assuming raw body when it’s an SNS envelope
Publisher Anything that sends to the topic publish / publish-batch Forgetting message attributes that filtering needs
Filter policy A per-subscription rule that drops non-matches Subscription attribute (JSON) Too strict / wrong scope → subscriber gets nothing
Message attributes Typed metadata beside the body --message-attributes Filtering on an attribute you never attached
Fan-out One publish → many subscribers The topic + N subscriptions Subscribing Lambda direct instead of via SQS
Raw message delivery Strip the SNS JSON envelope Subscription attribute Consumer breaks parsing .Message
DLQ (redrive) Where undeliverable messages land Subscription RedrivePolicy No DLQ → events dropped on endpoint failure
Topic policy Who may publish/subscribe Resource-based policy on the topic Cross-account publish denied; SQS can’t receive
KMS encryption Encrypt messages at rest KmsMasterKeyId on the topic Encrypted topic, SQS can’t decrypt → stuck

Learning objectives

By the end of this article you can:

Prerequisites & where this fits

You need an AWS account with permission to create SNS topics, SQS queues, IAM roles and Lambda functions (a personal or dev sandbox account — never straight into production). Install and configure the AWS CLI v2 (aws configure or aws sso login), and for the IaC half, Terraform ≥ 1.5. You should be comfortable reading JSON and running shell commands; you do not need prior SNS experience. Everything in the lab sits inside the always-free SNS tier (1 million requests/month) and free-tier SQS and Lambda, so the running cost is effectively ₹0 — the only line items that could bill a few paise are CloudWatch Logs and, if you send them, SMS/email deliveries (the lab uses neither).

Where this sits: SNS is one of the three application-integration primitives you wire everything else with. It pairs constantly with SQS (the durable queue you fan out into) and sits alongside EventBridge (the content-routing event bus). This article is the SNS-focused, hands-on companion to the broader Event-Driven Architecture on AWS: EventBridge, SQS, SNS, Lambda and Step Functions, which shows how all four fit together, and to AWS Lambda Patterns: Event-Driven Functions That Scale to Zero, which goes deep on the consumer side (idempotency, DLQs, batching). Two wave siblings drill into the neighbours this article only touches: a dedicated SQS Standard vs FIFO hands-on for the queue mechanics (visibility timeout, long polling, redrive) that the fan-out relies on, and an EventBridge rules & event bus hands-on for the content-routing alternative to SNS — reach for those when you need queue or bus depth beyond what you build here.

A quick map of who owns what, so when a fan-out misbehaves you look in the right place first:

Layer What lives here Who “owns” it What it can cause
Publisher The app/service calling Publish You Missing attributes → nothing matches a filter
Topic + topic policy The channel + who may publish/subscribe You (SNS) Cross-account publish denied; wrong ordering (Standard vs FIFO)
Subscription + filter The binding, its filter, raw delivery, DLQ You (SNS) Over-strict filter, unconfirmed sub, envelope confusion
Delivery + retries SNS pushing to the endpoint AWS (SNS) Retried then dropped with no DLQ
Endpoint policy SQS queue policy / Lambda permission / KMS key policy You (SQS/Lambda/KMS) “Confirmed but empty queue”; Lambda not invoked
Subscriber code The consumer processing the message You Duplicate side effects (at-least-once); parse errors

Core concepts

Five ideas make everything later obvious. Read them once; the deep sections just expand each.

A topic is a channel; publishers and subscribers never meet. You create a topic and get back a topic ARN (arn:aws:sns:ap-south-1:1234:orders-topic). Publishers call Publish against that ARN; subscribers call Subscribe against it. Neither side holds a reference to the other. That indirection is the point: the topic is the single stable contract, and both sides evolve independently behind it.

Publish is one beat; delivery is many. A single Publish call is accepted, durably stored across multiple Availability Zones, and then delivered independently to every matching subscription. Each delivery has its own protocol, its own retry state, its own filter evaluation and its own success/failure. One publish can produce a dozen deliveries; nine can succeed while three retry. You reason about the publish and each delivery as separate things.

SNS is push; SQS is pull — and they compose. SNS pushes to endpoints (it calls them). SQS is a queue that consumers poll. The most important pattern in this whole article — fan-out — is SNS pushing into several SQS queues so each downstream system can pull at its own pace, durably, with replay. SNS gives you the one-to-many broadcast; SQS gives each consumer a durable buffer. Together they are the standard way to decouple a producer from many independent, resilient consumers.

Delivery is at-least-once (Standard), so consumers must be idempotent. A Standard topic optimises for throughput and availability, which means a message can occasionally be delivered more than once and ordering is best-effort, not guaranteed. If your subscriber charges a card or inserts a row, design it to tolerate seeing the same message twice (a dedupe key, a conditional write). A FIFO topic trades throughput for strict ordering and exactly-once processing within a group — use it only when you truly need order.

Every gate is a policy, and there are three of them. Who may publish to and subscribe to a topic is the topic policy (resource-based, on the topic). Whether SNS may deliver into an SQS queue is the SQS queue policy (on the queue). Whether SNS may invoke a Lambda is the Lambda resource policy (on the function). The console wires these for you when you click “subscribe”; the CLI and Terraform do not, which is why “the subscription exists but the queue is empty” is the single most common fan-out bug.

The vocabulary in one table

Pin every moving part down before the deep dive. The glossary at the end repeats these for lookup; this is the mental model side by side:

Term One-line definition Where you set/see it Why it matters
Topic A named pub/sub channel create-topic → topic ARN The single stable contract both sides share
Subscription One endpoint bound to a topic subscribe Nothing is delivered until this exists and is confirmed
Protocol Delivery mechanism for a subscription subscribe --protocol Sets payload shape, confirmation, retry
Publisher Anything that calls Publish Your code / an AWS service Decoupled — never lists subscribers
Subscriber / endpoint The thing that receives (queue, fn, URL) subscribe --notification-endpoint Where the message actually lands
Fan-out One publish delivered to many subs Topic + N subscriptions The core one-to-many pattern
Message attributes Typed key/values beside the body --message-attributes Drive filtering; some protocols read them
Filter policy A rule that delivers only matches Subscription attribute Each subscriber gets only what it asked for
Raw message delivery Deliver the body without the SNS envelope Subscription attribute (SQS/HTTP) Consumer gets your JSON, not SNS metadata
Redrive policy (DLQ) Where undeliverable messages go Subscription attribute Captures failures instead of dropping them
Standard topic High-throughput, best-effort order, at-least-once Topic type The default; needs idempotent consumers
FIFO topic Strict order + dedup, lower throughput Topic type (.fifo) Only when ordering truly matters
Topic policy Who may publish/subscribe Resource-based policy Gates cross-account and service publishing

The publish → deliver contract

Everything SNS does is one of these four beats. Knowing which beat you’re debugging tells you where to look:

Beat What happens Owned by Fails as
Publish Message accepted, stored across AZs, returns a MessageId Publisher + SNS AuthorizationError (topic policy), InvalidParameter (size/attrs)
Filter Each subscription’s filter policy is evaluated SNS Silent — non-matches are simply not delivered
Deliver SNS pushes to each matching endpoint SNS + endpoint policy 403/AccessDenied (queue/fn policy), retries
Confirm / process Endpoint receives; consumer processes Subscriber Duplicates (at-least-once), parse errors, DLQ on give-up

The pub/sub model and topics

A topic is the unit you create, secure, and publish to. It has a type (Standard or FIFO), a policy, optional encryption, and a set of subscriptions. Nothing about a topic references its subscribers — that asymmetry is what lets you add and remove consumers freely.

Standard vs FIFO topics

The single biggest topic decision is Standard vs FIFO, and beginners over-reach for FIFO. FIFO guarantees ordering and deduplication, but at roughly 300 messages/second per topic and with hard constraints on which subscribers it can serve. Standard is virtually unlimited and is the right default for the overwhelming majority of fan-outs — you just make consumers idempotent.

Dimension Standard topic FIFO topic
Ordering Best-effort (not guaranteed) Strict, within a MessageGroupId
Delivery At-least-once (possible duplicates) Exactly-once (dedup within 5 min)
Deduplication None Content-based or MessageDeduplicationId
Throughput Virtually unlimited ~300 msg/s (up to 3,000 with batching)
Subscriber protocols SQS, Lambda, HTTP/S, email, SMS, push, Firehose SQS (FIFO for end-to-end order; Standard allowed), plus Lambda/HTTP/S/Firehose
Topic name orders-topic Must end in .fifo (orders-topic.fifo)
Message filtering Yes Yes
Typical use Notifications, fan-out, broadcast Bank ledgers, inventory, ordered state changes
Price (per 1M publishes) ~$0.50 ~$0.30 + FIFO delivery/data fees

The rule: use Standard and make consumers idempotent unless you can name a concrete correctness bug caused by out-of-order or duplicate processing. FIFO exists for cases like “apply account debits in order” where reordering is genuinely wrong — and even then, ordering is only preserved end-to-end if the FIFO topic delivers into FIFO SQS queues (a FIFO topic can fan out to Standard queues too, but you lose the guarantee at that hop).

FIFO ordering and deduplication knobs

If you do go FIFO, two parameters govern behaviour and beginners routinely misuse them:

Parameter What it controls Value Gotcha
MessageGroupId The ordering scope Any string (e.g. account-42) Order is only guaranteed within a group; pick a business key
MessageDeduplicationId The dedupe key Explicit id, or content hash Same id within 5 min = dropped as a duplicate
ContentBasedDeduplication Auto-hash the body as the dedupe id Topic attribute (true/false) If body varies but is logically the same event, you get dups
Throughput mode Per-group vs per-topic scaling Topic setting High-throughput FIFO shards by group id

Topic attributes and limits

A topic carries a handful of attributes you set once and mostly forget — but each maps to a real failure mode:

Attribute What it is Default Note
DisplayName Friendly name (used as SMS/email sender) none Required for SMS
Policy The topic access (resource) policy Owner-only Edit for cross-account / service publish
KmsMasterKeyId KMS key for SSE at rest none (unencrypted) Use alias/aws/sns or a CMK
DeliveryPolicy Default retry policy for HTTP/S subs Managed default Per-subscription policy overrides it
SignatureVersion Message signing version (1 or 2) 1 Use 2 (SHA-256) for HTTP/S verification
TracingConfig X-Ray active tracing PassThrough Set Active to trace publishes
FifoTopic Whether this is FIFO false Set at create time only; immutable
Quota Value Note
Max message size 256 KB (262,144 bytes) Body + attributes; use the Extended Client (S3-backed) for up to 2 GB
Subscriptions per topic 12,500,000 Effectively unlimited for fan-out
Topics per account 100,000 (default, soft) Raise via a quota request
FIFO topic throughput ~300 msg/s (3,000 batched) Per topic
Standard topic throughput Virtually unlimited Regional soft limits apply
Filter policy combinations 150 total value combinations Across all attribute names in the policy
Message attributes per message 10 (reserved) / effectively many Counts toward the 256 KB total

Subscriptions and delivery protocols

A subscription binds one endpoint to a topic under one protocol. The protocol decides three things a beginner must know up front: whether the subscription needs confirmation, what payload shape the endpoint receives, and how retries/DLQ behave.

The protocol matrix

Protocol Endpoint Confirmation Payload shape Raw delivery?
sqs SQS queue ARN Auto-confirmed SNS envelope JSON (or raw body) Yes
lambda Lambda function ARN Auto-confirmed SNS envelope in Records[].Sns No (always envelope)
http / https A URL Endpoint must confirm (token) SNS envelope JSON (or raw body) Yes
email An email address User clicks confirm link Plain-text message No
email-json An email address User clicks confirm link JSON envelope emailed No
sms A phone number (E.164) None (sandbox needs verified #) Plain SMS text No
application A mobile push endpoint ARN Per-endpoint Push payload (APNS/FCM) No
firehose A Firehose stream ARN (+ IAM role) Auto-confirmed Record to the stream Yes

Two facts save hours. First, SQS, Lambda and Firehose subscriptions auto-confirm, but email, email-json and HTTP/S do not — until confirmed they show SubscriptionArn: PendingConfirmation and receive nothing. Second, Lambda always receives the full SNS envelope (you read event['Records'][0]['Sns']['Message']); raw message delivery is not available for Lambda, only for SQS, HTTP/S and Firehose.

Confirmation behaviour

Protocol How confirmation works State until confirmed Common failure
SQS / Lambda / Firehose Auto (SNS trusts the ARN + policies) Confirmed immediately Not confirmation — it’s the endpoint policy
HTTP/S SNS POSTs a SubscriptionConfirmation; endpoint calls ConfirmSubscription with the token PendingConfirmation Endpoint 500s on the confirm POST, never subscribes
Email / email-json SNS emails a confirm link; the human clicks it PendingConfirmation Link ignored / in spam → silent
SMS No confirmation, but SMS sandbox requires verified destination numbers n/a Unverified number in sandbox → not sent

Subscription attributes you actually set

Every subscription has attributes that turn features on. These four are the ones you’ll touch constantly:

Attribute What it does Values Applies to
FilterPolicy Deliver only matching messages JSON policy All protocols
FilterPolicyScope Filter on attributes or the body MessageAttributes (default) / MessageBody All protocols
RawMessageDelivery Strip the SNS envelope true / false SQS, HTTP/S, Firehose
RedrivePolicy DLQ for undeliverable messages {"deadLetterTargetArn":"<sqs>"} All protocols
DeliveryPolicy Per-subscription retry policy JSON (HTTP/S) HTTP/S
SubscriptionRoleArn IAM role for delivery Role ARN Firehose

The fan-out pattern (and why SNS→SQS beats SNS→Lambda direct)

Fan-out is the reason most teams adopt SNS: one publish, delivered to many independent subscribers, each processing on its own terms. The canonical, production-grade shape is SNS → several SQS queues, with each queue owned by one consumer (a Lambda, an ECS service, an EC2 worker). This is what you build in the lab.

Why put SQS between SNS and your compute?

You can subscribe a Lambda directly to a topic — it works, and it’s tempting because it’s one fewer resource. But a direct Lambda subscription is an asynchronous push with all the fragility that implies. Fronting the Lambda with an SQS queue turns a fragile push into a durable, replayable pull. This is one of the most important design lessons in AWS integration, so here it is in full:

Concern SNS → Lambda (direct) SNS → SQS → Lambda
Delivery model Async push; SNS invokes Lambda SNS delivers to SQS; Lambda polls SQS
Buffering None — invoked immediately Durable buffer (up to 14 days retention)
Backpressure Lambda concurrency spikes with the topic SQS absorbs the spike; Lambda drains at its pace
Retry on failure SNS retries, then needs a subscription DLQ Message stays in the queue; SQS redrive to an SQS DLQ
Replay Hard — the event is gone once retries exhaust Easy — re-drive the DLQ back into the queue
Throttling / partial failure Whole invocation fails or succeeds ReportBatchItemFailures commits good records
Ordering / batching One message per invoke Batches (up to 10) per invoke
When it’s fine Truly fire-and-forget, loss-tolerant, low volume Almost everything else

The thesis: SNS gives you the broadcast; SQS gives each consumer durability, buffering and replay. If a consumer is down for an hour, the SQS-fronted version keeps its messages safe and drains them when it recovers; the direct-Lambda version depends entirely on SNS retries and a DLQ, and offers no easy replay. Use direct Lambda only for genuinely loss-tolerant notifications; use SNS→SQS for anything that matters.

Fan-out variations

Variation Shape Use when
Classic fan-out SNS → N SQS queues (one per consumer) Multiple independent consumers, each durable
Filtered fan-out SNS → N SQS, each with a filter policy Consumers want different subsets of events
Fan-out + direct SNS → SQS queues + a Lambda + an HTTP webhook Mix of durable and real-time subscribers
Cross-account fan-out SNS (acct A) → SQS (acct B, C) Platform team owns the topic; app teams own queues
Message-bus alt EventBridge instead of SNS You need content routing across many event types

Message filtering with filter policies

Fan-out delivers to every subscriber by default. Filter policies let each subscription say “only send me the messages I care about,” so SNS evaluates the policy per message and delivers only matches. This is how one topic serves many consumers with different interests without a firehose of irrelevant traffic — SQS-A gets order_placed, SQS-B gets premium-tier events, and neither pays to receive the other’s.

Filter policy scope: attributes vs body

A filter policy has a scope that decides what it inspects:

Scope Inspects Set as When to use
MessageAttributes (default) The typed message attributes you attach at publish FilterPolicyScope: MessageAttributes Publisher can tag messages with attributes
MessageBody The JSON message body itself FilterPolicyScope: MessageBody You can’t/won’t add attributes; filter on payload fields

Attribute-scoped filtering is the classic approach (cheap, explicit); body-scoped filtering (newer) lets you route on the payload directly — handy when the publisher is a service you don’t control and can’t make attach attributes.

Filter operators

A filter policy is a JSON object mapping attribute (or body-field) names to an array of match conditions. The value must satisfy at least one entry in the array (OR within a key), and all keys must match (AND across keys). The operators:

Operator Example Matches
Exact string "event": ["order_placed"] Exactly order_placed
Multiple values (OR) "tier": ["premium", "gold"] premium or gold
anything-but "event": [{"anything-but": ["test"]}] Anything except test
Prefix "region": [{"prefix": "ap-"}] ap-south-1, ap-east-1, …
Suffix "file": [{"suffix": ".jpg"}] Ends in .jpg
Numeric equals "amount": [{"numeric": ["=", 100]}] Exactly 100
Numeric range "amount": [{"numeric": [">", 1000]}] Greater than 1000
Numeric between "amount": [{"numeric": [">=", 100, "<", 500]}] 100–499
exists "coupon": [{"exists": true}] The attribute is present
exists: false "coupon": [{"exists": false}] The attribute is absent
IP address "ip": [{"cidr": "10.0.0.0/24"}] In the CIDR block

Worked filtering examples

Here is how the two lab queues route differently from the same topic:

Subscription Filter policy Receives Skips
SQS-A (orders) {"event": ["order_placed"]} Any message tagged event=order_placed order_shipped, order_cancelled
SQS-B (premium) {"tier": ["premium"]} Any message tagged tier=premium tier=standard
Lambda (all) (no filter policy) Every message Nothing
Big-orders (example) {"amount": [{"numeric": [">", 1000]}]} Orders over 1000 Small orders

A single publish with attributes event=order_placed, tier=premium, amount=2500 matches SQS-A (event), SQS-B (tier) and Lambda (no filter) — three deliveries from one publish. A publish with event=order_shipped, tier=standard matches only Lambda. That’s filtered fan-out in one sentence.

Filter policy limits and gotchas

Rule Detail Failure if violated
150-combination limit Total value combinations across all keys ≤ 150 InvalidParameter on set
AND across keys Every key in the policy must match Over-strict policy → nothing delivered
OR within a key Any array entry may match Too broad → noise
Missing attribute ≠ match If the attribute isn’t present, that key fails (unless exists:false) Silent drop
Scope must match publisher MessageAttributes scope needs attributes; MessageBody needs valid JSON Nothing matches
Numeric needs a number type amount must be a Number attribute, not String Numeric operator never matches
Case-sensitive Premiumpremium Silent drop

The number-one filtering bug: a policy that is correct but too strict, so the subscriber silently receives nothing. Always test a filter with a message you expect to match, and confirm with get-subscription-attributes.

Message attributes, structure and raw delivery

Three related concepts control what bytes a subscriber actually sees.

Message attributes

Message attributes are typed key/value pairs you attach beside the body. They exist for two reasons: to drive filtering (as above) and to carry metadata some protocols read (e.g. SMS sender id). They are part of the 256 KB total size.

Data type Example Used for
String {"DataType":"String","StringValue":"order_placed"} Most filtering
Number {"DataType":"Number","StringValue":"2500"} Numeric filter operators
String.Array {"DataType":"String.Array","StringValue":"[\"a\",\"b\"]"} Multi-value attributes
Binary {"DataType":"Binary","BinaryValue":<blob>} Opaque bytes (not filterable by content)

Message structure: per-protocol payloads

By default, every subscriber gets the same --message string. But with --message-structure json, you send a JSON object whose keys are protocols, so each subscriber type gets a tailored payload — a short SMS, a rich email, a full JSON to SQS:

--message-structure --message value Behaviour
(default / raw) A plain string Same body to every subscriber
json {"default":"...","sqs":"...","email":"...","sms":"...","https":"..."} Per-protocol body; default is the fallback

Example: {"default":"Order update","sms":"Order shipped!","email":"Your order has shipped. Track: ...","sqs":"{\"orderId\":\"1001\",\"status\":\"shipped\"}"} sends a terse SMS, a friendly email, and structured JSON to the queue — from one publish.

Raw message delivery: envelope vs raw

By default SNS wraps your message in a JSON envelope (metadata: Type, MessageId, TopicArn, Message, Timestamp, Signature, MessageAttributes). An SQS consumer then has to parse .Message out of that envelope. Raw message delivery strips the envelope so the subscriber receives exactly your body:

Aspect Envelope (default) Raw delivery (RawMessageDelivery=true)
What SQS receives {"Type":"Notification","Message":"<your body>","MessageAttributes":{...},...} <your body> verbatim
Consumer code Parse JSON, then read .Message Read the body directly
Message attributes Inside the envelope JSON Mapped to SQS message attributes
Best for You need SNS metadata/signature You just want the payload (most cases)
Applies to SQS, HTTP/S, Firehose only (not Lambda/email/SMS)

Turn raw delivery on for SQS subscriptions unless you specifically need the SNS metadata — it removes a whole class of “why is my JSON double-wrapped?” bugs.

Delivery retries and the subscription DLQ

SNS does not deliver once and forget. When an endpoint fails (a 500 from a webhook, a throttled Lambda, an SQS queue that briefly denies access), SNS retries on a policy that depends on the endpoint type. Whatever it still can’t deliver is either dropped or — if you configured a redrive policy — sent to a dead-letter queue.

The retry phases

SNS’s delivery retry policy has four phases, most relevant to HTTP/S where you can tune them:

Phase Parameter Meaning
Immediate (no delay) numNoDelayRetries Instant retries before any wait
Pre-backoff numMinDelayRetries at minDelayTarget Retries at the minimum delay
Backoff numRetries with backoffFunction Grows delay from min → max (linear/geometric/exponential)
Post-backoff numMaxDelayRetries at maxDelayTarget Retries at the maximum delay

Retry policy by endpoint type

Endpoint type Retry policy Customizable? After retries exhaust
SQS, Lambda, Firehose (AWS-managed) Aggressive, multi-phase, spans an extended window No Drop, or → subscription DLQ if set
HTTP/S (customer endpoint) Default: 3 retries, 20s linear; tunable up to 100 Yes (DeliveryPolicy) Drop, or → subscription DLQ if set
Email / SMS / push Managed by SNS No Drop (no DLQ for these)

The redrive policy (subscription DLQ)

A redrive policy on a subscription names an SQS queue that catches messages SNS could not deliver after all retries — instead of losing them, you keep them for inspection and replay. This is the “DLQ on a subscription” the lab attaches.

Aspect Detail
Set on The subscription (not the topic), via RedrivePolicy
Format {"deadLetterTargetArn":"arn:aws:sqs:...:kv-sns-dlq"}
DLQ type An SQS queue (Standard for Standard topics; FIFO for FIFO)
DLQ queue policy Must allow the SNS topic to sqs:SendMessage (same as any SNS→SQS)
What lands there The message plus metadata about the failed delivery attempt
Replay Read from the DLQ, fix the cause, re-publish or re-drive
Scope Per-subscription — each subscription can have its own DLQ

The rule mirrors Lambda’s: a subscription with no redrive policy silently drops messages it can’t deliver. Attach a DLQ to every subscription whose messages matter, and remember the DLQ’s own queue policy must let the topic write to it — a DLQ you can’t deliver into is no DLQ at all.

Encryption, topic policy and cross-account

KMS encryption at rest

SNS encrypts messages in transit over HTTPS always. For encryption at rest, enable server-side encryption (SSE) with a KMS key:

Option KmsMasterKeyId Cost Cross-service gotcha
Unencrypted (none) Free Fine for non-sensitive data
AWS-managed key alias/aws/sns Free (key), KMS API usage tiny Can’t be used across accounts
Customer-managed key (CMK) Your key ARN/alias ~$1/key/month + API calls The key policy must allow SNS and every subscriber

The classic encryption trap: you encrypt the topic with a CMK, and now SQS delivery fails silently because the SQS service (delivering on SNS’s behalf) can’t use the key. Fix: the CMK key policy must grant kms:Decrypt and kms:GenerateDataKey to the relevant service principals (and, for cross-account, the other account). Encrypting a topic is one line; making every consumer able to decrypt is the real work.

The topic access policy

The topic policy is a resource-based policy that answers “who may Publish and Subscribe?” The default policy allows only the topic owner’s account. You edit it to let another account or an AWS service publish:

Policy element Controls Typical value
Principal Who the statement is about An account ARN, or a service like s3.amazonaws.com
Action What they may do SNS:Publish, SNS:Subscribe
Resource Which topic The topic ARN
Condition aws:SourceArn Restrict to a specific source An S3 bucket / CloudWatch alarm ARN
Condition aws:SourceAccount Restrict to an account Prevents the confused-deputy problem

Cross-account publish and subscribe

Cross-account fan-out (platform team owns the topic; app teams own the queues) has three moving policies. Get all three and it works; miss one and you get a specific, diagnosable failure:

Direction What must allow what Where Failure if missing
Cross-account publish Topic policy allows account A’s principal to SNS:Publish Topic (acct B) AuthorizationErrorException on publish
Cross-account subscribe Topic policy allows account C to SNS:Subscribe Topic (acct B) AuthorizationError on subscribe
Deliver into SQS Queue policy allows sns.amazonaws.com to SendMessage with SourceArn=topic Queue (acct C) Subscription confirmed, queue stays empty
Invoke Lambda Function policy allows sns.amazonaws.com to InvokeFunction Function Lambda never invoked
Decrypt (if CMK) Key policy allows the subscriber/account KMS key Silent delivery failure

SNS vs SQS vs EventBridge — which integration?

These three are constantly confused. The one-liner: SQS is a queue (one-to-one, pull, buffer), SNS is a broadcaster (one-to-many, push, fan-out), EventBridge is a router (content-based routing across many event types and sources).

Question SQS SNS EventBridge
Pattern Point-to-point queue Pub/sub fan-out Event bus / content router
Delivery Consumer pulls SNS pushes EventBridge pushes to targets
Consumers One logical consumer group Many subscribers Many targets via rules
Routing None (all to one queue) Filter policies (attr/body) Rich content rules (JSON patterns)
Sources Anything that sends Anything that publishes 200+ AWS services, SaaS, custom
Latency Low Very low (ms) Low (slightly higher than SNS)
Throughput Very high Very high High (lower per-bus limits)
Ordering FIFO queues FIFO topics No ordering
Replay / archive DLQ / retention Subscription DLQ Archive + replay built in
Schema registry No No Yes
Typical use Decouple + buffer one consumer Broadcast to many; fan-out to SQS Route diverse events by content; SaaS ingestion
If you need… Reach for…
A durable buffer between one producer and one consumer SQS
One event delivered to many consumers, fast, with simple filtering SNS
Content-based routing across many event types/sources, with archive/replay and schemas EventBridge
Broadcast and durability/replay per consumer SNS → SQS (fan-out)
High-throughput, low-latency fan-out at the lowest cost SNS
To ingest events from a SaaS product (Datadog, Zendesk, Shopify) EventBridge

A useful mental model: many mature architectures use all three — EventBridge routes diverse events by content, SNS fans a chosen event out to many consumers, and each consumer sits behind its own SQS queue for durability. They compose; they don’t compete.

Architecture at a glance

The diagram below is the exact stack you build in the lab, drawn as a left-to-right flow. A publisher calls Publish on the SNS topic with a message plus message attributes (event, tier, amount). SNS evaluates each subscription’s filter policy and fans the message out: SQS-A keeps only event=order_placed, SQS-B keeps only tier=premium (both with raw message delivery so consumers get the body, not the envelope), and the Lambda subscription has no filter so it receives everything. Any delivery SNS can’t complete after its retries lands in the subscription DLQ (an SQS queue) instead of being dropped, and the whole thing is gated by the topic policy and KMS.

The six numbered badges mark the six places a fan-out fails, and the legend narrates each as symptom · confirm · fix — the same map as the troubleshooting playbook, drawn onto the architecture so you can see where each failure lives.

Amazon SNS fan-out request path: a producer app publishes one message with MessageAttributes to a Standard/FIFO SNS topic; SNS evaluates per-subscription filter policies and delivers to SQS-A (filtered event=order_placed, raw delivery), SQS-B (filtered tier=premium, raw delivery) and a Lambda notify function (no filter, receives all) as an async push, while undeliverable messages redrive to a subscription DLQ SQS queue; a topic policy plus KMS and the requirement that each SQS queue policy allow sns.amazonaws.com gate delivery, with six numbered badges marking message-attribute, Standard-vs-FIFO, filter-policy-scope, SQS-queue-policy, Lambda-permission and retry-redrive failure points.

Badge Failure class Lives at Playbook row
1 Missing/mismatched attributes The publish call rows 3, 4
2 Wrong topic type / ordering lost The topic rows 10, 11
3 Filter too strict / wrong scope The subscription filter rows 1, 2
4 SQS queue policy missing SNS The queue row 5
5 Lambda not invoked (permission) The function policy row 6
6 Messages dropped on failure Retries + redrive DLQ rows 8, 9

Real-world scenario

KloudCart, a mid-sized marketplace in Pune, had a checkout service that, on every order, synchronously called five downstream systems in a loop: inventory, billing, email, analytics and fraud. It worked until Black Friday, when the analytics endpoint slowed to 4 seconds per call. Because checkout awaited every downstream call, checkout latency ballooned to 12 seconds and the analytics timeout began throwing exceptions that rolled back otherwise-successful orders. One slow, non-critical consumer was taking down the critical path. Their senior engineer, Rohan, was tasked with decoupling it in a week.

He introduced an SNS topic, kc-order-events, and made checkout do exactly one thing after persisting the order: Publish an order_placed message with attributes event=order_placed, tier, and amount. Checkout latency dropped to the single publish call — about 8 milliseconds. Then he fanned out. Inventory, billing and analytics each got their own SQS queue subscribed to the topic; each queue was consumed by that team’s existing worker. The analytics slowdown now just meant its queue backed up harmlessly while checkout and the other consumers ran at full speed — the durable SQS buffer absorbed the lag that used to crash the order.

Filtering earned its keep immediately. The billing queue only needed paid orders, so Rohan added {"event": ["order_placed"], "payment": ["card", "upi"]} and billing stopped seeing cash-on-delivery noise. The fraud team only cared about high-value orders, so their subscription carried {"amount": [{"numeric": [">", 50000]}]} — one topic, five consumers, each receiving exactly its slice. He turned on raw message delivery for every SQS subscription so the workers read the order JSON directly instead of unwrapping the SNS envelope, which let him migrate the consumers with zero code changes to their message parsing.

Two failures during rollout mapped exactly to this article’s playbook. First, the billing queue stayed empty even though the subscription showed as confirmed — the SQS queue policy didn’t allow sns.amazonaws.com to SendMessage (the console adds it, but Rohan had used Terraform without an aws_sqs_queue_policy). Second, during a deploy the email consumer’s webhook was down for 20 minutes; without a subscription DLQ those notifications would have been dropped after retries. Rohan had attached a redrive policy to a kc-sns-dlq queue, so the 340 undelivered messages sat safely in the DLQ and were re-driven once the webhook recovered — zero lost emails. Post-migration: checkout p99 fell from 12 s to under 50 ms, a single slow consumer could no longer affect the order, and adding a new “loyalty points” consumer next quarter was a five-minute self-service subscribe with no checkout change. Rohan’s wiki note became team doctrine: “Publish once, fan out to a queue per consumer, filter at the subscription, and put a DLQ on everything that matters.”

Advantages and disadvantages

Advantages Disadvantages
Full producer/consumer decoupling (time + implementation) At-least-once on Standard — consumers must be idempotent
One-to-many fan-out with no publisher-side list No message persistence/replay in SNS itself (needs SQS/DLQ)
Very low latency, very high throughput Best-effort ordering on Standard (FIFO caps throughput)
Per-subscription filtering (attributes or body) 256 KB message cap (larger needs the S3 extended client)
Many native protocols, no glue code Consumers can’t pull/backlog — SNS pushes (front with SQS)
Generous always-free tier (1M requests/mo) No built-in schema registry or archive (EventBridge has these)
Serverless — nothing to run or scale Cross-account/KMS wiring is three policies deep
Subscription DLQ captures failures for replay Email/HTTP/S need confirmation — silent until confirmed

When each side matters: SNS dominates for one-to-many broadcast and fan-out — notifications, event distribution, “when X, do A and B and C.” The disadvantages bite when you need durable per-consumer backlogs (add SQS), content routing across many event types with replay/schemas (use EventBridge), or strict ordering at high volume (FIFO’s throughput ceiling fights you). The honest rule: reach for SNS whenever one event must reach many consumers quickly; put SQS behind each consumer that needs durability; and switch to EventBridge when routing logic, source variety, or archive/replay become the point.

Choose SNS when… Reconsider when…
One event must reach many consumers Only one consumer needs it (use SQS directly)
You want push delivery in milliseconds Consumers must pull at their own pace (front with SQS)
Simple attribute/body filtering is enough You need rich content routing (EventBridge)
Throughput matters more than strict order Global ordering is a correctness requirement (FIFO, or rethink)
You’re fanning out to SQS/Lambda/webhooks You need archive, replay and schemas out of the box (EventBridge)

Hands-on lab

You will build the diagram: a Standard topic that fans out to two SQS queues with different filter policies (raw delivery on) and a Lambda (no filter), with a subscription DLQ on the Lambda subscription. First with the aws CLI, then the identical stack as Terraform, then a clean teardown. Everything is free-tier. Pick a region and stick to it (this lab uses ap-south-1, Mumbai).

⚠️ Cost note: SNS publishes/deliveries to SQS and Lambda are free at this volume (1M requests/mo free tier). We deliberately avoid SMS and email (those do cost). The only paise-level items are CloudWatch Logs; we delete everything at the end.

What you’ll create

Resource Purpose Cost at lab volume
SNS topic kv-orders-topic The fan-out channel Free (within 1M req)
SQS queue kv-sqs-a Subscriber, filter event=order_placed Free
SQS queue kv-sqs-b Subscriber, filter tier=premium Free
SQS queue kv-sns-dlq Subscription dead-letter queue Free
Lambda kv-notify-fn + role Subscriber (no filter, logs the event) Free (free tier)
Subscriptions (×3) Bind the endpoints, with filters + DLQ Free

Part A — the CLI path

Step 1 — Create the topic. A Standard topic; capture its ARN:

TOPIC_ARN=$(aws sns create-topic \
  --name kv-orders-topic \
  --region ap-south-1 \
  --query TopicArn --output text)
echo "$TOPIC_ARN"
# arn:aws:sns:ap-south-1:111122223333:kv-orders-topic

Step 2 — Create the three SQS queues and grab their ARNs and URLs.

for q in kv-sqs-a kv-sqs-b kv-sns-dlq; do
  aws sqs create-queue --queue-name "$q" --region ap-south-1 >/dev/null
done

get_url() { aws sqs get-queue-url --queue-name "$1" --region ap-south-1 --query QueueUrl --output text; }
get_arn() { aws sqs get-queue-attributes --queue-url "$1" --attribute-names QueueArn --region ap-south-1 --query 'Attributes.QueueArn' --output text; }

A_URL=$(get_url kv-sqs-a);   A_ARN=$(get_arn "$A_URL")
B_URL=$(get_url kv-sqs-b);   B_ARN=$(get_arn "$B_URL")
DLQ_URL=$(get_url kv-sns-dlq); DLQ_ARN=$(get_arn "$DLQ_URL")

Step 3 — Let the topic deliver into each queue (the step the console hides). Each queue’s policy must allow sns.amazonaws.com to SendMessage, scoped to this topic. Do it for all three (including the DLQ, which SNS also writes to):

set_queue_policy() {
  local url="$1" arn="$2"
  local policy=$(cat <<JSON
{"Version":"2012-10-17","Statement":[{
  "Sid":"AllowSNS","Effect":"Allow",
  "Principal":{"Service":"sns.amazonaws.com"},
  "Action":"sqs:SendMessage","Resource":"$arn",
  "Condition":{"ArnEquals":{"aws:SourceArn":"$TOPIC_ARN"}}
}]}
JSON
)
  aws sqs set-queue-attributes --queue-url "$url" \
    --attributes "{\"Policy\":$(jq -Rs . <<<"$policy")}" \
    --region ap-south-1
}
set_queue_policy "$A_URL" "$A_ARN"
set_queue_policy "$B_URL" "$B_ARN"
set_queue_policy "$DLQ_URL" "$DLQ_ARN"

Step 4 — Subscribe SQS-A with a filter policy + raw delivery. SQS subscriptions auto-confirm, so this is live immediately:

SUB_A=$(aws sns subscribe \
  --topic-arn "$TOPIC_ARN" \
  --protocol sqs \
  --notification-endpoint "$A_ARN" \
  --attributes '{"RawMessageDelivery":"true","FilterPolicy":"{\"event\":[\"order_placed\"]}"}' \
  --region ap-south-1 \
  --query SubscriptionArn --output text)

Step 5 — Subscribe SQS-B with a different filter policy.

SUB_B=$(aws sns subscribe \
  --topic-arn "$TOPIC_ARN" \
  --protocol sqs \
  --notification-endpoint "$B_ARN" \
  --attributes '{"RawMessageDelivery":"true","FilterPolicy":"{\"tier\":[\"premium\"]}"}' \
  --region ap-south-1 \
  --query SubscriptionArn --output text)

Step 6 — Create the Lambda and let SNS invoke it. A trivial handler that logs the event, an execution role for logs, then the resource-based permission SNS needs:

cat > notify.py <<'PY'
import json
def handler(event, context):
    for rec in event["Records"]:
        msg = rec["Sns"]["Message"]          # Lambda always gets the SNS envelope
        print(f"notify-fn received: {msg}")
    return {"ok": True}
PY
zip notify.zip notify.py

cat > trust.json <<'JSON'
{"Version":"2012-10-17","Statement":[{"Effect":"Allow",
 "Principal":{"Service":"lambda.amazonaws.com"},"Action":"sts:AssumeRole"}]}
JSON
aws iam create-role --role-name kv-notify-role \
  --assume-role-policy-document file://trust.json >/dev/null
aws iam attach-role-policy --role-name kv-notify-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
sleep 10   # let the role propagate

ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
aws lambda create-function --function-name kv-notify-fn \
  --runtime python3.12 --architectures arm64 \
  --handler notify.handler --zip-file fileb://notify.zip \
  --role arn:aws:iam::${ACCOUNT_ID}:role/kv-notify-role \
  --region ap-south-1 >/dev/null

# The resource-based policy that lets SNS invoke the function:
aws lambda add-permission --function-name kv-notify-fn \
  --statement-id sns-invoke --action lambda:InvokeFunction \
  --principal sns.amazonaws.com --source-arn "$TOPIC_ARN" \
  --region ap-south-1

Step 7 — Subscribe the Lambda (no filter → receives everything) and attach a DLQ.

FN_ARN=arn:aws:lambda:ap-south-1:${ACCOUNT_ID}:function:kv-notify-fn
SUB_L=$(aws sns subscribe \
  --topic-arn "$TOPIC_ARN" --protocol lambda \
  --notification-endpoint "$FN_ARN" \
  --region ap-south-1 --query SubscriptionArn --output text)

# Subscription DLQ: undeliverable messages go to kv-sns-dlq instead of being dropped
aws sns set-subscription-attributes \
  --subscription-arn "$SUB_L" \
  --attribute-name RedrivePolicy \
  --attribute-value "{\"deadLetterTargetArn\":\"$DLQ_ARN\"}" \
  --region ap-south-1

Step 8 — Publish and watch filtering route the same topic three ways. First a message that matches both queues and the Lambda:

aws sns publish --topic-arn "$TOPIC_ARN" \
  --message '{"orderId":"1001","amount":2500}' \
  --message-attributes '{"event":{"DataType":"String","StringValue":"order_placed"},"tier":{"DataType":"String","StringValue":"premium"}}' \
  --region ap-south-1

Now one that matches neither queue (only the unfiltered Lambda):

aws sns publish --topic-arn "$TOPIC_ARN" \
  --message '{"orderId":"1002","status":"shipped"}' \
  --message-attributes '{"event":{"DataType":"String","StringValue":"order_shipped"},"tier":{"DataType":"String","StringValue":"standard"}}' \
  --region ap-south-1

Step 9 — Verify the routing. Read each queue: SQS-A should have only order 1001; SQS-B should have only order 1001; the Lambda logs should show both 1001 and 1002.

aws sqs receive-message --queue-url "$A_URL" --max-number-of-messages 10 --region ap-south-1 \
  --query 'Messages[].Body' --output text
# {"orderId":"1001","amount":2500}      <-- raw body, no SNS envelope

aws sqs receive-message --queue-url "$B_URL" --max-number-of-messages 10 --region ap-south-1 \
  --query 'Messages[].Body' --output text
# {"orderId":"1001","amount":2500}      <-- 1002 was filtered out (tier=standard)

aws logs tail /aws/lambda/kv-notify-fn --region ap-south-1 --since 5m
# notify-fn received: {"orderId":"1001","amount":2500}
# notify-fn received: {"orderId":"1002","status":"shipped"}   <-- Lambda has no filter

That contrast — SQS-A/B each hold only their matching message while the Lambda holds both — is filtered fan-out. Notice the SQS bodies are your raw JSON (raw delivery on), while the Lambda received the full envelope (rec["Sns"]["Message"]).

Step 10 — Tear down. Delete in dependency order:

aws sns unsubscribe --subscription-arn "$SUB_A" --region ap-south-1
aws sns unsubscribe --subscription-arn "$SUB_B" --region ap-south-1
aws sns unsubscribe --subscription-arn "$SUB_L" --region ap-south-1
aws sns delete-topic --topic-arn "$TOPIC_ARN" --region ap-south-1
for u in "$A_URL" "$B_URL" "$DLQ_URL"; do aws sqs delete-queue --queue-url "$u" --region ap-south-1; done
aws lambda delete-function --function-name kv-notify-fn --region ap-south-1
aws logs delete-log-group --log-group-name /aws/lambda/kv-notify-fn --region ap-south-1
aws iam detach-role-policy --role-name kv-notify-role \
  --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
aws iam delete-role --role-name kv-notify-role
Teardown step Command Why it must be explicit
Unsubscribe (×3) sns unsubscribe Deleting the topic leaves subs Deleted but tidy first
Delete topic sns delete-topic Stops all fan-out
Delete queues (×3) sqs delete-queue Queues persist and can hold messages otherwise
Delete function lambda delete-function Stops invocations
Delete log group logs delete-log-group Logs bill after the function is gone
Detach + delete role detach-role-policy then delete-role Can’t delete a role with attached policies

Part B — the same stack as Terraform

The CLI teaches the moving parts; Terraform is how you keep them. Note the three things people forget in IaC that the console does automatically: the SQS queue policy (aws_sqs_queue_policy), the Lambda permission (aws_lambda_permission), and the filter policy / raw delivery / redrive on the subscription.

terraform {
  required_providers {
    aws     = { source = "hashicorp/aws", version = "~> 5.0" }
    archive = { source = "hashicorp/archive", version = "~> 2.4" }
  }
}
provider "aws" { region = "ap-south-1" }

data "aws_caller_identity" "me" {}

# --- Topic ---
resource "aws_sns_topic" "orders" {
  name = "kv-orders-topic"
}

# --- Queues (A, B, DLQ) ---
resource "aws_sqs_queue" "a"   { name = "kv-sqs-a" }
resource "aws_sqs_queue" "b"   { name = "kv-sqs-b" }
resource "aws_sqs_queue" "dlq" { name = "kv-sns-dlq" }

# --- Queue policies: allow THIS topic to SendMessage ---
data "aws_iam_policy_document" "allow_sns" {
  for_each = { a = aws_sqs_queue.a.arn, b = aws_sqs_queue.b.arn, dlq = aws_sqs_queue.dlq.arn }
  statement {
    effect    = "Allow"
    actions   = ["sqs:SendMessage"]
    resources = [each.value]
    principals { type = "Service", identifiers = ["sns.amazonaws.com"] }
    condition {
      test     = "ArnEquals"
      variable = "aws:SourceArn"
      values   = [aws_sns_topic.orders.arn]
    }
  }
}
resource "aws_sqs_queue_policy" "a"   { queue_url = aws_sqs_queue.a.id,   policy = data.aws_iam_policy_document.allow_sns["a"].json }
resource "aws_sqs_queue_policy" "b"   { queue_url = aws_sqs_queue.b.id,   policy = data.aws_iam_policy_document.allow_sns["b"].json }
resource "aws_sqs_queue_policy" "dlq" { queue_url = aws_sqs_queue.dlq.id, policy = data.aws_iam_policy_document.allow_sns["dlq"].json }

# --- SQS subscriptions with distinct filter policies + raw delivery ---
resource "aws_sns_topic_subscription" "a" {
  topic_arn            = aws_sns_topic.orders.arn
  protocol             = "sqs"
  endpoint             = aws_sqs_queue.a.arn
  raw_message_delivery = true
  filter_policy        = jsonencode({ event = ["order_placed"] })
  depends_on           = [aws_sqs_queue_policy.a]
}
resource "aws_sns_topic_subscription" "b" {
  topic_arn            = aws_sns_topic.orders.arn
  protocol             = "sqs"
  endpoint             = aws_sqs_queue.b.arn
  raw_message_delivery = true
  filter_policy        = jsonencode({ tier = ["premium"] })
  depends_on           = [aws_sqs_queue_policy.b]
}

# --- Lambda subscriber (no filter) + its permission + a subscription DLQ ---
data "archive_file" "fn" {
  type        = "zip"
  source_file = "${path.module}/notify.py"
  output_path = "${path.module}/notify.zip"
}
resource "aws_iam_role" "fn" {
  name = "kv-notify-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{ Effect = "Allow", Principal = { Service = "lambda.amazonaws.com" }, Action = "sts:AssumeRole" }]
  })
}
resource "aws_iam_role_policy_attachment" "logs" {
  role       = aws_iam_role.fn.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "fn" {
  function_name    = "kv-notify-fn"
  role             = aws_iam_role.fn.arn
  runtime          = "python3.12"
  architectures    = ["arm64"]
  handler          = "notify.handler"
  filename         = data.archive_file.fn.output_path
  source_code_hash = data.archive_file.fn.output_base64sha256
}
resource "aws_lambda_permission" "sns" {
  statement_id  = "sns-invoke"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.fn.function_name
  principal     = "sns.amazonaws.com"
  source_arn    = aws_sns_topic.orders.arn
}
resource "aws_sns_topic_subscription" "lambda" {
  topic_arn  = aws_sns_topic.orders.arn
  protocol   = "lambda"
  endpoint   = aws_lambda_function.fn.arn
  redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.dlq.arn })
  depends_on = [aws_lambda_permission.sns]
}
terraform init
terraform apply        # review the plan, type yes
# publish a test message the same way as Step 8, then:
terraform destroy      # one-command teardown
Terraform detail Why it’s there If you omit it
aws_sqs_queue_policy Lets the topic deliver into each queue Subscription confirmed, queue empty
aws_lambda_permission Lets SNS invoke the function Lambda never invoked
raw_message_delivery = true SQS gets the body, not the envelope Consumers parse SNS metadata
filter_policy = jsonencode(...) Per-subscription routing Every queue gets every message
redrive_policy on the sub Subscription DLQ Undeliverable messages dropped
depends_on on policy/permission Order creation before the subscription First deliveries fail

Common mistakes & troubleshooting

This is the section you’ll return to. Match your symptom, run the confirm command, apply the fix.

# Symptom Root cause Confirm (exact command / console path) Fix
1 Subscriber receives nothing, others do Filter policy too strict / wrong values aws sns get-subscription-attributes --subscription-arn <arn> → read FilterPolicy Loosen the policy; test with a message you know matches
2 Filtering ignores attributes you send FilterPolicyScope mismatch (body vs attributes) Check FilterPolicyScope; check whether you sent attributes or only a body Match scope to how you publish (MessageAttributes vs MessageBody)
3 Numeric filter never matches Attribute sent as String, not Number Inspect the publish --message-attributes DataType Publish the attribute as {"DataType":"Number",...}
4 Subscriber gets every message No filter policy on that subscription get-subscription-attributes shows no FilterPolicy Add a FilterPolicy (or accept it’s the “all” subscriber)
5 Subscription confirmed, SQS queue empty Queue policy doesn’t allow sns.amazonaws.com to SendMessage aws sqs get-queue-attributes --attribute-names Policy — no SNS statement Add the queue policy with Principal sns.amazonaws.com + SourceArn
6 Lambda never invoked Missing resource-based permission for SNS aws lambda get-policy --function-name <fn> — no sns.amazonaws.com statement aws lambda add-permission ... --principal sns.amazonaws.com --source-arn <topic>
7 SQS consumer chokes parsing JSON Envelope, not raw body (double-wrapped) Inspect the SQS body — starts with {"Type":"Notification"... Set RawMessageDelivery=true, or parse .Message
8 Messages disappear on endpoint failure No redrive policy; retried then dropped get-subscription-attributes → no RedrivePolicy Add a RedrivePolicy pointing to an SQS DLQ
9 DLQ configured but stays empty on failures DLQ queue policy doesn’t allow the topic to write sqs get-queue-attributes --attribute-names Policy on the DLQ Add the same SNS SendMessage policy to the DLQ queue
10 Duplicate processing Standard topic is at-least-once; consumer not idempotent Compare MessageIds / your business key in logs Make the consumer idempotent (dedupe key / conditional write)
11 Ordering lost Standard topic (best-effort), or FIFO → Standard queue Check topic type (.fifo?) and the subscribed queue type Use a FIFO topic and FIFO queues; set MessageGroupId
12 Email/HTTP sub never delivers Subscription stuck PendingConfirmation aws sns list-subscriptions-by-topicSubscriptionArn: PendingConfirmation Click the email link / have the endpoint call ConfirmSubscription
13 Cross-account publish denied Topic policy doesn’t allow the other account aws sns get-topic-attributes → read Policy Add SNS:Publish for the account principal to the topic policy
14 Encrypted topic, SQS gets nothing CMK key policy doesn’t allow SQS/subscriber KMS console → key policy; CloudTrail kms:Decrypt denials Grant the service/account kms:Decrypt+GenerateDataKey on the CMK
15 Publish fails AuthorizationErrorException Publisher IAM or topic policy forbids it aws sns get-topic-attributes; check the caller’s IAM Grant sns:Publish in IAM and/or the topic policy
16 FIFO publish rejected Missing MessageGroupId (required for FIFO) Error: “MessageGroupId is required” Add --message-group-id; add dedup id or content-based dedup

SNS / delivery error reference

Error / status Meaning Likely cause Fix
AuthorizationErrorException Not authorized IAM or topic policy denies Publish/Subscribe Grant the action in IAM and/or the topic policy
InvalidParameterException (size) Message too big Body+attributes > 256 KB Shrink, or use the SNS Extended Client (S3)
InvalidParameter (filter) Bad filter policy > 150 combinations / malformed JSON Simplify the policy; validate JSON
NotFoundException Topic/subscription missing Wrong ARN / region Verify the ARN and --region
EndpointDisabled Push endpoint disabled Mobile token invalid/expired Re-register the device endpoint
KMSAccessDenied (in delivery) Can’t use the CMK Key policy missing the service/account Fix the KMS key policy
PendingConfirmation (state) Sub not active Email/HTTP not confirmed Confirm the subscription
Throttled / FIFO 300/s FIFO throughput cap Publishing faster than 300 msg/s Batch (publish-batch), shard by group, or use Standard

Delivery status: how to actually see what SNS did

SNS delivery is invisible until you turn on delivery status logging — an IAM role plus a sample rate that writes success/failure to CloudWatch Logs. It is the single best tool for “did SNS even try, and what did the endpoint say?”

Attribute What it enables Set on
SQSSuccessFeedbackRoleArn / ...FailureFeedbackRoleArn Log SQS delivery outcomes Topic
LambdaSuccessFeedbackRoleArn / Failure... Log Lambda delivery outcomes Topic
HTTPSuccessFeedbackRoleArn / Failure... Log HTTP/S delivery + status codes Topic
*SuccessFeedbackSampleRate 0–100 (% of successes to log) Topic

The three nastiest, explained

“Subscription confirmed but the queue is empty” (row 5) wastes the most hours because the subscription genuinely exists and looks healthy — so you debug SNS when the problem is the SQS queue policy. SNS delivers to SQS by calling sqs:SendMessage as the SNS service principal; if the queue’s resource policy doesn’t allow sns.amazonaws.com (scoped to your topic ARN), every delivery is denied and the queue stays empty. The console adds this policy when you subscribe; the CLI and Terraform do not. Always attach the queue policy, and use aws sqs get-queue-attributes --attribute-names Policy to confirm the SNS statement is there.

“The subscriber receives nothing” (rows 1–2) is almost always a filter policy that is correct-but-too-strict, or scoped to the wrong thing. Remember the semantics: AND across keys, OR within a key, and a missing attribute fails its key (unless you used exists:false). If you filter on MessageAttributes but publish only a body (or vice-versa via FilterPolicyScope), nothing matches. Debug by publishing a message you are certain should match, then dump get-subscription-attributes and compare field-by-field. A numeric filter silently failing is usually a String attribute where a Number was required.

“Messages disappear under an outage” (rows 8–9) catches teams who added fan-out but not resilience. SNS retries, but a Standard topic does not persist messages after retries exhaust — without a redrive policy the message is gone. And the DLQ itself is an SQS queue that SNS must be allowed to write to, so a DLQ whose queue policy doesn’t grant the topic SendMessage is a DLQ that also silently drops. Attach a redrive policy to every subscription that matters, give the DLQ the same SNS-allow policy as any other queue, and you turn “lost forever” into “sitting in the DLQ, ready to replay.”

Best practices

Security notes

SNS’s security surface is small but every piece gates delivery. Get these right from the start:

Control What to do Why
Topic access policy Grant Publish/Subscribe to named principals with SourceArn/SourceAccount conditions Stops the confused-deputy problem and unwanted publishers
KMS encryption at rest Set KmsMasterKeyId; use a CMK for sensitive data Encrypts messages in the topic; CMK gives you audit + key control
KMS key policy for consumers Allow SQS/Lambda/cross-account principals kms:Decrypt+GenerateDataKey Otherwise delivery silently fails on an encrypted topic
SQS queue policy Allow only sns.amazonaws.com + your topic ARN The queue accepts SNS deliveries but nothing else
Lambda resource policy Scope InvokeFunction to sns.amazonaws.com + the topic ARN Only your topic can invoke the function
HTTPS + signature verification Verify the SNS message signature (SignatureVersion 2) on HTTP/S endpoints Confirms the message really came from SNS
Least-privilege publishers Grant sns:Publish only on the specific topic ARN A compromised publisher can’t spam other topics
No PII in message bodies for SMS/email Keep sensitive data out of low-control channels SMS/email leave the AWS trust boundary
VPC endpoints (PrivateLink) Publish to SNS over a VPC endpoint Keeps publish traffic off the public internet
CloudTrail on Audit Publish, Subscribe, SetTopicAttributes Detect tampering and rogue subscriptions

The two you’ll get wrong first: encrypting a topic with a CMK and forgetting to let SQS decrypt (silent delivery failure), and leaving a topic policy that allows Subscribe too broadly (someone subscribes their own endpoint to your event stream). Lock the policy to named principals with conditions.

Cost & sizing

SNS pricing is refreshingly simple: you pay per request (a publish or a delivery) plus per-delivery fees that depend on the protocol, and the free tier covers most beginner and small-production usage. SQS/Lambda deliveries are the cheap, high-value path.

Cost driver How it’s charged (Standard, ap-south-1 approx.) Lever
Requests First 1,000,000/month free, then ~$0.50 per 1M Batch with publish-batch (10/call)
SQS deliveries Free (SQS charges its own requests) The reason SNS→SQS is the default
Lambda deliveries Free (Lambda charges its own invocations)
HTTP/S deliveries ~$0.60 per 1M Fine at scale
Email / email-json ~$2.00 per 100,000 Use sparingly; not for high volume
SMS Per-message, country-specific (India relatively high) Use only for real user alerts
Mobile push ~$0.50 per 1M Cheap for app notifications
FIFO ~$0.30 per 1M publishes + delivery + payload/GB fees Only when you need ordering
Data transfer Standard AWS egress applies Keep consumers in-region

A worked example to make it concrete (rough INR at ~₹84/USD):

Scenario Publishes/mo Deliveries Protocols Est. monthly cost
This lab ~10 ~30 SQS + Lambda ₹0 (free tier)
Order fan-out 1,000,000 5,000,000 (5 SQS) SQS ~₹0 (within/near free; SQS deliveries free)
Notifications 2,000,000 2,000,000 HTTP/S webhook Requests ~$0.50 + HTTP ~$1.20 ≈ ₹145
Email digests 500,000 500,000 email Email ~$10 ≈ ₹840 (email is the pricey bit)
SMS OTPs 100,000 100,000 SMS Dominated by per-SMS India rates (budget separately)

The takeaways: SQS and Lambda deliveries are free, which is exactly why the SNS→SQS fan-out is both the most robust and the cheapest pattern; email and SMS are the expensive protocols (price them before you fan out to them at volume); and 1M requests/month free means small apps run at ₹0. To right-size, batch publishes where you can, filter at the subscription so you don’t pay to deliver noise, and keep consumers in-region to avoid egress.

Interview & exam questions

1. What is Amazon SNS in one sentence, and how does it differ from SQS? SNS is a managed pub/sub service: publishers send a message to a topic and SNS pushes a copy to every subscriber (one-to-many, push). SQS is a queue: one producer, one consumer group, and the consumer pulls (one-to-one, pull, durable buffer). SNS broadcasts; SQS buffers. (CLF-C02, SAA-C03)

2. Explain the fan-out pattern and why you’d put SQS between SNS and a consumer. Fan-out is one publish delivered to many subscribers. Fronting each consumer with its own SQS queue adds a durable buffer, backpressure, easy replay, and per-record retry/redrive — so a slow or down consumer doesn’t lose messages or affect others. A direct Lambda subscription has none of that beyond SNS retries and a DLQ. (SAA-C03, DVA-C02)

3. When do you need a FIFO topic, and what’s the cost? Only when strict ordering and exactly-once processing within a group are correctness requirements (e.g. financial ledgers). The cost is throughput — about 300 messages/second — and the constraint that end-to-end ordering requires FIFO SQS queues as subscribers. Otherwise use Standard and make consumers idempotent. (SAA-C03, DVA-C02)

4. How does message filtering work, and what are the two scopes? Each subscription can carry a filter policy that SNS evaluates per message, delivering only matches. The scope is either MessageAttributes (filter on typed attributes attached at publish) or MessageBody (filter on JSON fields in the payload). Semantics are AND across keys, OR within a key. (DVA-C02)

5. Your SQS queue is subscribed and confirmed but receives nothing. First hypothesis? The SQS queue’s resource policy doesn’t allow sns.amazonaws.com to sqs:SendMessage for the topic ARN. The console adds this automatically; CLI/Terraform don’t. Confirm with sqs get-queue-attributes --attribute-names Policy. (DVA-C02, SCS)

6. What is raw message delivery and when do you enable it? By default SNS wraps the body in a JSON envelope (metadata + Message). Raw message delivery (SQS/HTTP/Firehose only) strips the envelope so the subscriber receives your body verbatim, with attributes mapped to native SQS attributes. Enable it unless you need the SNS signature/metadata. (DVA-C02)

7. How do you stop SNS from losing messages when an endpoint is down? Attach a redrive policy (subscription DLQ) pointing to an SQS queue; after retries exhaust, undeliverable messages go there instead of being dropped, ready to inspect and replay. Ensure the DLQ’s queue policy allows the topic to write to it. (DVA-C02, SAA-C03)

8. Describe delivery/retry differences between endpoint types. For AWS-managed endpoints (SQS, Lambda, Firehose), SNS uses an aggressive managed multi-phase retry policy. For HTTP/S you can customize the delivery policy (immediate, pre-backoff, backoff, post-backoff; default ~3 retries at 20s). Email/SMS/push retries are managed and have no DLQ. (DVA-C02)

9. How do you publish cross-account, and what three policies are involved? The topic policy must allow the publisher’s account SNS:Publish (and a subscriber account SNS:Subscribe); the subscriber’s SQS queue policy must allow sns.amazonaws.com to SendMessage; and if the topic uses a CMK, the key policy must allow the relevant principals to decrypt. (SCS, SAA-C03)

10. SNS vs EventBridge — when each? SNS for high-throughput, low-latency, one-to-many fan-out with simple attribute/body filtering. EventBridge for content-based routing across many event types and 200+ AWS/SaaS sources, with a schema registry and built-in archive/replay. Many designs use EventBridge to route and SNS to fan out. (SAA-C03, DVA-C02)

11. Why must Standard-topic consumers be idempotent? Standard topics deliver at-least-once, so a consumer can occasionally see the same message twice; ordering is best-effort too. If processing has side effects (charging, inserting), design for duplicates with a dedupe key or conditional write. (DVA-C02)

12. What’s the maximum SNS message size and how do you exceed it? 256 KB (body + attributes). For larger payloads, use the SNS Extended Client Library, which stores the payload in S3 and sends a reference through SNS (up to 2 GB). (DVA-C02)

Quick check

  1. A publisher sends one message with attributes event=order_placed, tier=standard. SQS-A filters {"event":["order_placed"]}, SQS-B filters {"tier":["premium"]}, Lambda has no filter. Which endpoints receive it?
  2. You subscribed an SQS queue via the CLI; it’s confirmed but empty. What single resource is almost certainly misconfigured?
  3. Name two concrete advantages of SNS→SQS→Lambda over subscribing the Lambda directly.
  4. Your consumer is receiving {"Type":"Notification","Message":"..."} and choking on it. What subscription setting fixes this?
  5. You must guarantee ordered, exactly-once processing of account transactions. What topic type and which subscriber queue type do you use?

Answers

  1. SQS-A (event matches) and Lambda (no filter). SQS-B does nottier=standard fails its tier=premium filter.
  2. The SQS queue’s resource policy — it must allow sns.amazonaws.com to sqs:SendMessage scoped to the topic ARN. The console adds this; the CLI/Terraform don’t.
  3. Any two of: a durable buffer (up to 14 days) so a down consumer loses nothing; backpressure so spikes queue rather than overwhelm; easy replay by re-driving the DLQ; batching and partial-failure handling on the poll.
  4. Turn on raw message delivery (RawMessageDelivery=true) so the consumer gets the body directly instead of the SNS envelope (or parse .Message out of the envelope).
  5. A FIFO topic delivering to FIFO SQS queues, with a MessageGroupId that scopes ordering (e.g. per account) and deduplication enabled.

Glossary

Term Definition
Pub/Sub A messaging pattern where publishers send to a topic and subscribers receive, without knowing each other.
Topic A named SNS channel that publishers send to and subscribers attach to; Standard or FIFO.
Subscription A binding of one endpoint to a topic under one protocol; must be confirmed to receive.
Protocol The delivery mechanism for a subscription: SQS, Lambda, HTTP/S, email, SMS, application (push) or Firehose.
Fan-out Delivering a single published message to many subscribers in parallel.
Message attribute A typed key/value (String/Number/Binary) attached beside the body, used for filtering and metadata.
Filter policy A per-subscription JSON rule that delivers only matching messages; scoped to attributes or the body.
Raw message delivery A subscription option (SQS/HTTP/Firehose) that strips the SNS envelope so the body is delivered verbatim.
SNS envelope The default JSON wrapper (Type, MessageId, Message, Signature, attributes) around a delivered message.
Redrive policy (DLQ) A subscription setting naming an SQS queue that captures messages SNS couldn’t deliver after retries.
Standard topic High-throughput topic with best-effort ordering and at-least-once delivery.
FIFO topic Ordered, deduplicated topic (~300 msg/s) whose name ends in .fifo; pairs with FIFO queues.
MessageGroupId The FIFO ordering scope; messages within a group are strictly ordered.
Topic policy The resource-based policy on a topic controlling who may publish and subscribe.
Delivery status logging Optional CloudWatch logging of per-delivery success/failure via feedback IAM roles.
Extended Client Library A client that stores payloads > 256 KB in S3 and sends a reference through SNS (up to 2 GB).

Next steps

AWSSNSSQSPub/SubFan-OutMessage FilteringEvent-DrivenTerraform
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading