Skip to content

SNS: publish once, deliver to independent subscribers

12 min read · main part only; folded sections are optional

After this you can answer: How do you fan one business event out safely, and distinguish publication, delivery and processing failures?

The API publishes to the topic; it does not enumerate consumers. A topic is a named SNS destination for published messages. A subscription connects a topic to a receiving endpoint, here an SQS queue. The pattern is publish/subscribe, or pub/sub: producers publish independently of the consumers that subscribe. Fan-out means delivering one event to several independent subscribers.

flowchart LR
A["MiniTickets API outbox relay"]:::app -->|"publishes — API task role sns:Publish"| T["minitickets-dev-booking-events"]:::service
T -->|"raw JSON — SQS subscription + queue policy sqs:SendMessage"| E["minitickets-dev-jobs"]:::aws
T -->|"raw JSON — SQS subscription + queue policy sqs:SendMessage"| Q["minitickets-dev-tickets"]:::aws

Legend: highlighted = SNS; app = MiniTickets code; AWS = independently stored queue copies.

Both queue policies name sns.amazonaws.com, with aws:SourceArn equal to this topic and aws:SourceAccount equal to our account. The shared map names these connections api-service-publishes, topic-to-jobs-queue and topic-to-tickets-queue. Open in the map →.

Two workers reading one SQS queue would compete for messages. They would not each receive every event. Two subscribed queues are what give email and tickets separate copies.

  1. A booking transaction writes the booking and an outbox row, a durable record that an event still needs publication, together in Postgres. This prevents a committed booking from depending on an immediate SNS network call.
  2. The API relay validates the row using BookingConfirmedMessage. It calls SNS with the event serialized as JSON text.
  3. SNS authorizes sns:Publish for the API task role on the booking topic. Acceptance returns a transport message ID. The relay can then mark its outbox row sent.
  4. SNS evaluates each subscription. With no filter policy, both current subscriptions receive every booking event.
  5. SNS sends a copy to each SQS queue under that queue’s resource policy. Raw message delivery removes the SNS JSON wrapper, leaving our original booking JSON as the SQS body.
  6. Lambda’s event source mappings poll each queue and invoke the appropriate worker. SNS’s work ends at delivery to SQS; the queues and workers own subsequent processing retries.
sequenceDiagram
participant A as API outbox relay
box rgb(237,233,254) SNS
participant T as booking-events SNS
end
participant E as jobs SQS
participant Q as tickets SQS
A->>T: Publish booking.confirmed, sns:Publish
T-->>A: Accepted, MessageId
Note over A,T: Acceptance does not mean consumer success
T->>E: SendMessage, raw JSON, queue policy
E-->>T: Stored
T->>Q: SendMessage, raw JSON, queue policy
Q-->>T: Stored
Note over E,Q: Independent polling, retries and DLQs

Legend: solid arrows = requests; dashed arrows = responses; delivery branches progress independently.

If SNS accepted a publish but the relay lost the response, the outbox retries. That can publish a second copy. SNS standard topics do not solve this ambiguity; consumers must recognize the business event across transport IDs. Idempotency means repeating an operation has the same intended business result as performing it once. The outbox chapter explains the database boundary.

SNS separates accepting a publication from delivering to individual subscribers. The service stores published messages across separate servers and data centers, then applies each subscription’s delivery settings. An unavailable endpoint does not force the producer to hold its request open until every consumer recovers. AWS SNS durability features.

That separation is not a transaction across all recipients. One queue can receive an event while another rejects it. There is no rollback that removes the first copy when the second delivery fails. Design consumers to catch up independently, and distinguish eventual business completion from successful publication.

A standard topic supports several endpoint protocols, including SQS, Lambda, HTTP/S and email. Ordering is best effort and duplicates are possible. FIFO, meaning first in, first out, topics add ordering per MessageGroupId and deduplication using MessageDeduplicationId. A group identifies the messages whose order matters; separate groups permit parallel work. Use a business event ID for deduplication rather than a newly generated ID on each retry. AWS FIFO grouping.

SNS FIFO can deliver to FIFO or standard SQS queues. A standard queue still permits reordered or duplicate delivery to its consumer, so an ordered topic alone cannot guarantee ordered processing. Lambda consumption normally goes through a subscribed queue rather than a direct FIFO-topic Lambda subscription. AWS FIFO delivery.

A filter policy chooses which messages one subscription accepts. Its scope can inspect message attributes, which are metadata fields outside the body, or JSON properties inside the body. Attribute scope is the default. Within a key, alternatives can match; several required keys must all match. Missing required fields reject the event. Unlisted fields do not prevent delivery. AWS subscription filter rules.

Filtering is routing, not retryable failure. A message excluded by a filter does not go to that subscription’s DLQ. Test accepted and rejected examples before a filter change, including older schema versions. New filters can take time to propagate; do not treat the first result after an update as proof of the final behavior.

These values are at the time of writing; check the AWS docs before production sizing. Verified on 19 October 2026. They are AWS documented values, not measured MiniTickets capacity.

ItemCurrent valueSource / consequence
Default maximum publish size256 KiB, including body and attributesOur topic does not override it; large payloads
Configurable maximum publish sizeUp to 1 MiB using MaximumMessageSizeAdded September 2026; AWS announcement
Topics configured above 256 KiBUp to 100 subscriptions; only SQS, Lambda and FirehoseExisting defaults remain 256 KiB; large payload rules
SQS raw-delivery attributesAt most 10Too many are treated as a client-side delivery error; raw delivery
Subscription filter updateCan take up to 15 minutes to fully applyFilter policy consistency
Retryable server-side delivery errors to SQS/LambdaUp to 100,015 attempts over roughly 23 daysThis does not apply to every error; delivery retries
Publish quota in us-east-1Default 30,000 messages/second per account/Region, adjustableShared across publish calls and topic types; verify the account’s actual quota; AWS quotas
FIFO duplicate-send interval5 minutesDoes not atomically commit external actions; deduplication

The current SNS maximum is no longer universally 256 KiB. Equally, a queue supporting 1 MiB does not increase the topic’s configured limit. Body bytes and attribute bytes count together. Keep booking messages small even when a larger ceiling exists. AWS publish validation.

This fan-out is implemented in the repository. The committed data, compute and entry switches are currently off. No live AWS verification was performed for this page. Messaging resources are created independently of those compute/data switches.

infra/src/jobs/booking-events.ts exports createBookingEvents. It creates the standard topic minitickets-dev-booking-events, a resource policy for each existing queue and two SQS subscriptions with rawMessageDelivery: true. It sets neither a filter policy nor a subscription DLQ. Both consumers therefore receive the full current event stream.

The queue policy is created before its subscription. The function returns a topic ARN that depends on both subscription resources existing. That dependency prevents a newly deployed publisher from starting before its intended delivery routes are configured. It cannot protect against a later manual policy change or removed subscription.

apps/api/src/queues/jobs.ts exports publishBookingConfirmed. Its SNS client uses maxAttempts: 1; the durable outbox owns retry timing instead of hiding retries inside the client. apps/api/src/services/outbox.ts exports relayOnce, which validates identity fields and schedules failed publication again with a persisted due time.

packages/shared/src/schemas/jobs.ts defines the contract’s type, version, booking identity and the snapshot needed by consumers. The email address is personal data. Workers log IDs and correlation IDs rather than the complete body. Event-carried state transfer means including the data consumers need in the event, avoiding a new Postgres dependency; the trade-off is a booking-time snapshot.

MiniTickets also uses a separate SNS topic, minitickets-dev-alerts, for operational emails. Its topic policy permits our CloudWatch alarms to publish, restricted by source account and alarm ARN pattern. Its email subscription needs human confirmation. It is a separate use of SNS from booking-event fan-out.

ConnectionRequired permission or configuration
API relay → booking topicAPI task role’s send-jobs policy: sns:Publish on this topic only, in infra/src/compute/iam.ts
Booking topic → email queueSQS subscription plus jobs queue policy: sns.amazonaws.com, sqs:SendMessage, matching topic ARN and account
Booking topic → tickets queueSeparate subscription and separate policy on the tickets queue with the same restrictions
Queue → workerLambda event source mapping and that worker’s execution-role SQS read/delete permissions; SNS publish permission does not grant these
CloudWatch alarms → alerts topicTopic resource policy allows cloudwatch.amazonaws.com to call sns:Publish for our alarms

A resource policy is a permission document attached to the target resource; an identity policy is attached to the calling role. The queue’s policy authorizes SNS as the caller. Granting sqs:SendMessage to the API role would not repair a missing SNS queue policy. Restricting the service principal to our topic/account prevents another source from using the same service to send unwanted messages. AWS SNS-to-SQS access example.

flowchart TB
F["SNS cannot deliver to queue"]:::failure --> C{"Delivery error"}:::service
C -->|"client error; no retry"| L["No subscription DLQ: message discarded"]:::failure
C -->|"server retries exhausted"| L
C -.->|"alternative: subscription redrivePolicy + sqs:SendMessage"| D["Subscription DLQ (designed, not built)"]:::designed
D -.->|"visible count"| A["Delivery DLQ alarm (designed, not built)"]:::designed
A -.->|"SNS notification"| E["Alert email (designed, not built)"]:::designed

Legend: highlighted = SNS decision; red = actual loss path without a subscription DLQ; dashed = proposed protection.

What you seeWhyFix
Publisher gets AccessDeniedIts identity lacks sns:Publish, targets the wrong ARN, or another applicable policy denies itInspect the calling API task role, exact resource and any explicit deny; leave the outbox row pending
Publish succeeds but one queue gets nothingQueue policy denies SNS, subscription points elsewhere, or endpoint was deletedCheck the subscription and that queue’s own source conditions; a queue DLQ cannot capture a message never delivered there
NumberOfNotificationsFilteredOut increasesFilter scope, names or values do not match the publisher’s messageCompare body versus attributes, types and schema versions; filtered-out events are not delivery failures
Worker rejects every messageRaw delivery was disabled, so it receives an SNS wrapper instead of BookingConfirmedMessageRestore the intended contract or explicitly unwrap before validation; replay after repair
InvalidParameter on publishCombined body/attributes exceed topic size or another parameter is invalidRead the specific error and inspect size/configuration; do not endlessly retry unchanged invalid input

SNS distinguishes client-side errors, such as a deleted endpoint or invalid access policy, from server-side errors, such as a temporarily unavailable SQS service. Client errors are not retried. Server errors use the protocol’s retry policy. An SNS subscription DLQ catches delivery failures before a consumer receives the message; an SQS consumer DLQ catches messages delivered successfully but repeatedly failing processing. Our code has only the latter. AWS subscription DLQ behavior.

For SQS subscriptions, NumberOfNotificationsFailed increments when SNS stops attempts, not at every retry. Delivery metrics also do not prove email was sent or a ticket created. Monitor publications, subscriber deliveries and each consumer’s outcomes separately. Additional subscription DLQs and SNS delivery alarms would be proposed improvements, not current resources. AWS metric meanings.

At ten times bookings, the worker or its downstream service is likely to saturate before the regional SNS publish quota. A healthy SNS delivery rate can coexist with growing SQS age. Keep bounded consumer concurrency and measure how quickly each queue drains. More subscribers multiply deliveries and downstream work even though the API still makes one publish.

At one hundred times traffic, look beyond SNS throughput. Our outbox relay publishes within a database delivery transaction. Long SNS calls can occupy database connections and delay other relays; bounded request timeouts and durable retries matter. Raising a regional quota does not increase Postgres capacity or a provider’s email allowance.

Version the event contract and roll consumers forward before making incompatible producer changes. Add only the fields each subscriber is permitted to see. A filter policy chooses events; it does not remove sensitive fields from the events it accepts. If consumers have different data-access requirements, publish a smaller shared event or separate appropriately scoped events.

For long-term recovery, our standard topic has no built-in historical replay. SNS FIFO now supports an optional topic archive and subscription replay, so “SNS never supports replay” is outdated. That feature is not enabled here and is not a reason by itself to require FIFO ordering. Choose recovery requirements first. AWS standard versus FIFO archive options.

  • “Publish success means all consumers succeeded.” It means SNS accepted the event. Queue delivery and business completion are later boundaries.
  • “Our queue DLQ catches SNS authorization failures.” It catches failed consumption. Delivery failures need a DLQ on the subscription, which we have not configured.
  • “SNS retries every error for 23 days.” That long retry schedule concerns retryable AWS-managed endpoint failures. Client errors are not retried.
  • “SNS is push, so it replaces SQS.” SNS distributes copies; SQS retains each consumer’s backlog for polling and controlled processing.

How would you add fraud review without coupling it to confirmation email or ticket generation?

Add a separate fraud queue subscribed to the booking topic, with its own SNS queue policy, worker concurrency and consumer DLQ. Keep a stable booking/event identity so fraud retries are idempotent. Add a subscription DLQ for failures to deliver into that queue. These additions are designed, not built. Existing subscribers keep their independent failure paths.

flowchart TB
T["minitickets-dev-booking-events"]:::service -->|"existing subscriptions; queue policies"| B["jobs SQS + tickets SQS"]:::aws
T -.->|"new subscription; SNS sqs:SendMessage policy"| Q["Fraud SQS (designed, not built)"]:::designed
Q -.->|"event source mapping; execution-role receive/delete"| W["Fraud worker (designed, not built)"]:::designed
T -.->|"new subscription redrivePolicy; DLQ queue policy"| D["Delivery DLQ (designed, not built)"]:::designed

Legend: highlighted = existing SNS topic; AWS = existing queues; dashed = proposed fraud resources.

If fraud review must approve before issuing tickets, independent fan-out is insufficient: ticket generation would already have started. Publish a later approval event or use an explicit workflow to enforce that dependency. The business requirement determines whether consumers are independent.

Use SQS for queued work with one logical processing outcome. Use SNS for straightforward fan-out to known subscriptions. Consider EventBridge when many producers, event patterns, AWS/SaaS sources or archive/replay requirements make an event bus useful. An event bus receives events and evaluates routing rules. Use Step Functions when you must track ordered workflow progress and compensation. These services can be combined; they solve different responsibilities. AWS messaging decision guide.

Why raw message delivery?
Our worker already expects the booking JSON directly. Raw delivery preserves that contract instead of adding SNS metadata and a nested Message string to parse.
Can two subscriptions on one topic have different filters?
Yes. Each subscription selects its own accepted messages. Configure body or attribute scope deliberately, and remember that a filter mismatch is not a retryable failure.
What happens if a new subscription is added tomorrow?
It receives matching future publications. Our standard topic does not automatically send yesterday’s events to it; historical backfill needs a separate recovery source or archive design.
Does FIFO remove the need for idempotency?
No. It orders groups and suppresses repeated sends within the deduplication window, but cannot atomically commit queue deletion together with an external business action.
Why keep the outbox if SNS is durable?
SNS can preserve events only after accepting them. The outbox covers the gap between committing a booking and successfully publishing its event.
  • One topic publication creates independent delivery attempts for its subscriptions.
  • Publish acceptance, queue delivery and business completion are different successes.
  • SNS needs permission on each target queue; publisher permission cannot replace it.
  • Subscription DLQs cover delivery failure; queue DLQs cover processing failure.
  • Filters select events, and standard topics need a separate historical replay plan.
Advanced: IAM, config, CLI and code

In SNS → Topics → minitickets-dev-booking-events → Subscriptions, verify two SQS endpoints. Open each subscription to check raw delivery, filter scope and redrive policy. Then inspect the policy on each SQS queue. The absence of a subscription DLQ is a visible configuration gap, not evidence that consumer DLQs cover it.

Read-only inspection, using ARNs already available in your environment:

Terminal window
aws sns get-topic-attributes --topic-arn "$BOOKING_EVENTS_TOPIC_ARN"
aws sns list-subscriptions-by-topic --topic-arn "$BOOKING_EVENTS_TOPIC_ARN"
aws sns get-subscription-attributes --subscription-arn "$SUBSCRIPTION_ARN"

In CloudWatch → Metrics → SNS → Topic Metrics, compare NumberOfMessagesPublished, NumberOfNotificationsDelivered and filter/failure metrics. If subscription DLQs are added, monitor both NumberOfNotificationsRedrivenToDlq and NumberOfNotificationsFailedToRedriveToDlq; the recovery queue itself also needs a working resource policy. Avoid assuming delivered counts prove customer outcomes.

FIFO deduplication scope can be topic-wide or message-group-wide depending on FifoThroughputScope. The five-minute deduplication window is distinct from retention and from the business operation’s idempotency period. AWS’s exactly-once delivery wording also has conditions about permissions, timely deletion, filters and network acknowledgements. AWS FIFO conditions.

Continue with the shorter fan-out chapter or SNS-delivery diagnostic case.

Answer these without scrolling up. If one feels hard, reread that section.

  1. Which success boundary does our relay record after SNS accepts an event?
  2. Why would two workers on one queue fail to provide fan-out?
  3. What do raw delivery and filter scope each change?
  4. Which DLQ catches a queue-policy denial, and does our source configure it?
  5. When does adding a dependent business step require more than an independent subscription?

Redraw it from memory: the API relay, booking topic and two queues, labeling publisher IAM and both queue policies

Try this: inspect both subscription endpoints and confirm raw delivery and the absence of subscription redrive policies without publishing a message

Read this book online: https://book.theversehub.in/