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

Visitor
Website
API Gateway
Lambda
SNS
Lambda
SQS
SES
Lambda
DynamoDB
  1. 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. 2

    Browser calls API Gateway

    Clicking the button sends a POST /notify request with no body data beyond an internal note string — nothing personal is sent.

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