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. A CloudWatch alarm watches that dead-letter queue itself — if a message sits there for more than 2 of its 4-day retention days, an SNS-triggered Lambda emails an alert, repeating every 6 hours until it's resolved, so a stuck message doesn't just quietly expire unnoticed.
-
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
Pay-per-use the whole way through — no server sits idle, so an unclicked demo costs nothing. These are estimates based on public AWS list pricing, not a guarantee.
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.
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 GSI instead of a Scan for "recent deliveries"
A plain table Scan reads items in whatever order they
happen to sit on disk, not by recency — once the table
holds more than a couple hundred rows, a Scan can genuinely
miss the newest ones. A GSI (a constant partition key +
triggeredAt as the sort key) turns "recent
deliveries" into an actual sorted Query instead of a guess.
TTL still handles cleanup, but correctness of the read never
depends on the table staying small.
Polling instead of a WebSocket
This project deliberately reaches for a simple 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.
A CloudWatch alarm on the queue, not a polling Lambda
Detecting "a message has been sitting in the dead-letter
queue too long" could be done with a scheduled Lambda that
periodically peeks at the queue — but SQS already publishes
exactly that number as a CloudWatch metric
(ApproximateAgeOfOldestMessage), so a metric
alarm gets the same detection with no polling code, no
schedule to maintain, and no risk of the check itself
silently stopping.
Architecture Diagram
Full Architecture Diagram
The complete AWS architecture diagram for this project, built with draw.io. Click it to expand full screen.