Guide

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 attempt

Each stage has one job:

  1. 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.
  2. Validation checks authentication, the request schema and that the template exists. It also suppresses blocked users.
  3. Preferences drop disabled channels, apply quiet hours and frequency caps. The preferences page covers this stage.
  4. Router writes one work item per user, channel and notification onto that channel's queue.
  5. 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

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

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