Case studies
The notification pipeline
The stages a notification passes through, from the ingress API to a provider, with per-channel queues, a channel adapter layer and a durable in-app inbox.
A notification system takes an event, such as an order shipping, and delivers a message to the right user on the right channel. The core is a pipeline. Each stage checks or transforms the message, and only the last stage does the slow work of calling an outside provider.
Concept
A queue holds work until a worker is ready. A provider is an outside service that actually sends the message: APNs or FCM for push, an email service for email, and an SMS gateway such as Twilio for text messages. See queues for the basics.
The big idea
Every channel is its own lane: a push queue, an email queue and an SMS queue. Each lane has its own speed and its own provider limits. The shape looks like this:
Walking through it
Take one event: an order shipped, with a push and an in-app message for user 42.
POST /notify templateId=order_shipped userId=42 priority=transactional
1. validate auth ok, template exists, user not suppressed
2. preferences push on, quiet hours off, so send push and inbox
3. route one item on the push queue, one row written to the inbox
4. worker render the template, call the push provider, log the attemptEach stage has one job:
- Ingress API accepts the request. It carries the user or audience, the template, the data, the channels, the priority, an optional send time and an idempotency key.
- Validation checks authentication, the request schema and that the template exists. It also suppresses blocked users.
- Preferences drop disabled channels, apply quiet hours and frequency caps. The preferences page covers this stage.
- Router writes one work item per user, channel and notification onto that channel's queue.
- Workers render the template, call the provider, record the outcome, and retry or dead-letter failures.
The sizes set the design. At a peak of about 10 million notifications a minute, the load is about 167,000 per second (10,000,000 ÷ 60). With a mix of 70% push, 25% email and 5% SMS, that is about 117,000 push, 42,000 email and 8,300 SMS per second at peak. No SMS provider takes that. Channel mix and per-provider limits therefore shape the queues, not the other way round.
Adapters and the in-app inbox
Wrap each provider behind a thin channel adapter with one method, send(rendered, destination), that returns a result. The adapter owns authentication, payload shape and error mapping, such as Unregistered, Throttle or Transient. Workers stay channel-agnostic, so you can run two email providers side by side during a migration.
In-app notifications are a separate durable feed: a row with the user, the created time, the payload and the read time. The same router writes them, and they don't need APNs. Marking something read clears the badge without a round trip to a push provider.
Common mistakes
- Calling providers from the request path. Provider delays then become user-facing delays.
- One queue for all channels. A throttled channel holds up the rest.
- Putting provider-specific code in workers. Use the adapter, so a provider change stays local.
Check yourself
-
Why use one queue per channel rather than one queue for everything?
Each channel has its own speed and rate limits. A throttled channel must not hold up the others.
-
At 10 million notifications a minute, roughly how many per second is that?
About 167,000 per second, which is 10,000,000 ÷ 60.
Key takeaways
- A notification moves through validation, preferences, routing, per-channel queues and workers.
- Each channel gets its own queue, because each has its own limits.
- Providers sit behind adapters, so workers don't know which provider they call.
- The in-app inbox is a durable feed written by the same router.
Further learning
Next: preferences and quiet hours, which decides what enters the pipeline at all.
Related
Message queues
A durable buffer between the code that creates work and the code that does it. Start here for the map of queue concepts.
Idempotency and retries
Making operations safe to retry, with idempotency keys, deduplicating consumers and jittered backoff, so at-least-once delivery never means a double charge.
Rate limiting
Why systems cap how much work a client can send, which algorithm counts, where the limit lives and what the client is told. Start here for the rate-limiting map.