Project
SNS Notification Fan-Out
Click the button below and one message publishes to an SNS topic — then watch it deliver down two independent paths at once: a direct Lambda subscriber and a buffered SQS-backed subscriber. This is a live AWS architecture, not a mockup, and it collects nothing about you to run.
Demo
Trigger a Test Notification
This button sends a real request to API Gateway, which publishes one message to an SNS topic. The table below polls a second endpoint and fills in as each subscriber finishes its own work.
Privacy notice: no personal information is collected here — not even an email address. The email branch of this demo always notifies my own inbox, purely to prove that branch of the fan-out fired.
Live delivery log
No notifications triggered yet this session.| Triggered |
Message ID
The publisher Lambda generates a random id the moment it
publishes to SNS — it's not from AWS itself. Both
subscribers get that same id attached to the message,
which is how the two independent branches get matched
back into a single row instead of showing up as two
unrelated events.
|
SQS → Lambda → DynamoDB | Lambda → SES |
|---|
Architecture
How it works
One publish, two independent deliveries — click any step to jump to it, or let it play through on its own. Steps 1-4 are the setup; at step 5, SNS fans out to both branches at once.
-
1
Visitor loads the page
This static site — including this page and its button — is served from S3, the same way as the rest of the site.
-
2
Browser calls API Gateway
Clicking the button sends a
POST /notifyrequest with no body data beyond an internal note string — nothing personal is sent. -
3
Lambda publishes to SNS
The publisher function generates a message id, publishes one message to the SNS topic, and immediately returns that id to the browser — it doesn't wait for either subscriber to finish.
-
4
SNS fans the message out
The topic delivers the message to both of its subscribers at once — a direct Lambda subscription and a separate SQS queue, both reached in the same step next.
-
5
Both branches receive it simultaneously
One Lambda is subscribed directly to the topic — no queue in front of it, so it runs the moment SNS delivers the message. At the same time, the message also lands in an SQS queue subscribed to the topic. That branch trades a little latency for durability: if its consumer Lambda is briefly unavailable, the message waits in the queue instead of being lost, retrying up to three times before landing in a dead-letter queue.
-
6
Each branch finishes its own way
The direct-subscription Lambda calls SES to send a one-line email to a fixed admin address. In parallel, a second Lambda — triggered by the queue rather than the topic directly — picks up the message once it's available. Both write their own row to the same DynamoDB table, keyed by message id.
-
7
DynamoDB stores the delivery row
This is the SQS branch's write landing in the table — combined with the SES branch's write from step 6, that's what lets the live log table above show two independent checkmarks per row.
Use cases
Where this pattern fits
SNS fan-out shows up anywhere one event needs to trigger several unrelated pieces of work without those pieces knowing about each other.
Multi-channel alerting
One event — a deploy, an alarm, an order — needs to reach several destinations at once: email, chat, a queue for downstream processing, a dashboard.
Decoupling microservices
A publisher doesn't need to know who's listening or how many subscribers there are — new consumers can be added later without changing the publishing service at all.
Mixing delivery guarantees
Some subscribers want low latency and can tolerate an occasional miss; others need durability and retries. SNS lets each subscriber pick its own trade-off, as shown by the two paths on this page.
Pricing
What this actually costs
Same pay-per-use model as the rest of this site's projects — no server sits idle, so an unclicked demo costs nothing. These are estimates based on public AWS list pricing, not a guarantee.
Low volume
~$0/mo
Up to a few thousand triggers a month — comfortably inside the free tiers for SNS, SQS, Lambda, and DynamoDB on-demand.
Moderate volume
~$1/mo
Tens of thousands of triggers a month — mostly SES sending costs once its free tier is used up; SNS/SQS/Lambda stay near free.
High volume
~$5–10/mo
Hundreds of thousands of triggers a month — Lambda invocations and SES sends start to add up across both fan-out branches.
What drives the cost
- SNS — billed per publish and per delivery to each subscriber; the free tier covers 1M publishes and 100K HTTP/S deliveries a month, and Lambda/SQS deliveries are free entirely.
- SQS — billed per million requests; the free tier covers the first 1M every month.
- Lambda — billed per request and per millisecond; four small functions here, each running in well under 200ms.
- DynamoDB — on-demand billing, and rows expire automatically after 24 hours via TTL, so storage never grows.
- SES — billed per 1,000 emails sent, after the free tier.
Design decisions
Why I built it this way
A few choices here aren't the only way to build this — here's the reasoning behind them.
No visitor email collected, even though SES is in the loop
A real SNS topic subscription is static — you can't dynamically subscribe a visitor's address to receive one message and then unsubscribe them, at least not without a confirmation step that would break the instant-feedback point of the demo. Rather than fake that, the SES branch always emails me, and the visitor verifies the fan-out worked by watching the live log table instead.
Two subscriber types, not two of the same kind
It would have been simpler to subscribe two Lambdas directly to the topic. Using a direct subscription for one branch and an SQS-buffered one for the other is more work, but it's also the actual point of this project — showing that SNS lets each subscriber pick its own latency/durability trade-off, not just that it can fan out at all.
One shared DynamoDB table, split by sort key
The notifier and logger Lambdas don't know about each other and never coordinate directly — they just both write to the same table, keyed by message id and their own subscriber name. That's what lets a single Query reconstruct which branches completed for a given click, without either function needing awareness of the other.
A Scan seemed fine, until it wasn't
The first version read "recent" rows back with a plain table
Scan, reasoning that daily TTL cleanup would keep the table
small enough for it not to matter. That held up during early
testing, then quietly stopped being true — a Scan reads items
in whatever order they happen to sit in on disk, not by
recency, and once the table had grown past a couple hundred
rows a Scan could genuinely miss the newest ones. Fixed by
adding a GSI (a constant partition key + triggeredAt
as the sort key) so reads are an actual sorted Query instead of
a guess. TTL still handles cleanup — it just never was the
thing keeping reads correct.
Polling instead of a WebSocket
The movie poll project already covers a real-time WebSocket pattern. This project deliberately reaches for a simpler tool — the browser polling a REST endpoint every couple seconds — since a persistent connection isn't warranted just to watch two Lambdas each run once.