Breaking-News Push Alert Architecture

Breaking-news alerts invert every other push playbook. The message is worthless minutes after the event, so it needs maximum urgency, a very low TTL, and a collapse key — and it must fan out to millions of subscribers near-simultaneously. This guide covers the headers, the keys, and the throughput architecture that make that possible.

Quick answer

For breaking news, send with urgency: high, a low TTL (60–600 s) so stale alerts are dropped rather than delivered late, and a topic/collapse key so a correction supersedes the original instead of stacking. The dominant engineering problem isn’t the payload — it’s fan-out throughput: a single story can require millions of sends in seconds, so the architecture is a fast queue feeding batched, rate-limited workers. Keep the payload under the 4 KB limit with aes128gcm encoding; carry a headline and a deep link, fetch the article on click.

Why this playbook is different

The three axes flip. Where cart and SaaS messages tolerate latency, breaking news cannot — a TTL of minutes is a feature, not a limitation, because a late alert is misinformation. Where the others fan out per user or per segment, news fans out to your entire eligible audience at once. The hard constraint moves from payload size to raw throughput. This is one of three playbooks in the use-case playbooks reference, sitting at the extreme high-urgency, low-TTL, high-fan-out corner.

Breaking-news fan-out architecture An editorial event enqueues one job that fans out across a queue to batched worker pools, which send rate-limited requests to push services with high urgency and low TTL. Editorial event Fan-out queue Worker pool Worker pool Worker pool Push services FCM / Autopush / APNs One event → millions of sends: urgency=high, low TTL, shared topic key
One editorial event enqueues a single fan-out job; batched worker pools send rate-limited, high-urgency, low-TTL requests to the push services.

Headers and keys

Three send-time settings define a breaking-news alert:

  • urgency: high tells the push service to deliver immediately and wake the device if needed.
  • Low TTL (60–600 s) so an alert that can’t be delivered promptly is discarded rather than arriving stale.
  • topic (collapse key) shared across an event so a follow-up or correction replaces an undelivered original — an offline device that reconnects gets only the latest, not a pile.

The TTL is not a retry budget — it is an expiry contract with the push service. RFC 8030 lets the service hold a message for at most TTL seconds while the device is unreachable, and once that window closes the message is discarded, silently and without a delivery receipt. That is the behaviour you want for news: a device that comes back online forty minutes after a stabbing was reported should not be told about it as if it had just happened. Choose the value from the shelf life of the fact, not from a hope that everyone gets it — the reasoning behind picking a specific number is in setting optimal TTL values for time-sensitive alerts.

A 300-second TTL decides who receives a news alert An alert is sent at minute zero with a TTL of 300 seconds; a device that wakes at minute three is delivered the alert, while a device that wakes at minute twenty-five falls outside the window and the push service discards the message. TTL 300 s: the expiry is the feature deliverable expired — discarded by the push service Device A Device B wakes at 3 min → alert shown, still true wakes at 25 min → nothing arrives 201 Created, then displayed no receipt, no stale headline sent 10 min 20 30 40 min Losing Device B is the correct outcome, not a delivery failure to fix
With a five-minute TTL the push service drops what it cannot deliver in time — which is exactly why breaking news uses one.

Corrections travel on the same topic key

A breaking story is rarely one message. The first alert says a vote is scheduled; twenty minutes later it is delayed; an hour after that it is cancelled. Each of those is a send, and each carries the same topic: story-<id> value. The push service keeps at most one undelivered message per topic per subscription, so a device that has been dark through all three reconnects to a single notification carrying the latest state. Drop the topic key and the same device wakes to three notifications in chronological order, meaning the user reads the wrong headline first and has to reconcile the rest themselves.

Collapse key behaviour for a corrected alert An original alert and a later correction both carry topic story-4821; the push service holds one slot per topic, discards the undelivered original and delivers only the correction when the device reconnects. One topic key, one slot, one notification v1 “Vote scheduled” topic: story-4821 v2 “Vote delayed” topic: story-4821 Push service: one slot per topic v1 superseded never displayed v2 held for delivery within the TTL window Device reconnects shows “Vote delayed” only one notification, not two Without the shared topic the device wakes to both, oldest headline first Collapse applies only while a message is still undelivered
Collapse is a queue-level replacement: it can supersede an undelivered alert, never one already on screen.

Implementation

The fan-out enqueues one job; workers pull batches and send with the right headers, reading VAPID details from the environment — never hardcode the public key.

const webpush = require('web-push');

webpush.setVapidDetails(
  'mailto:alerts@yourdomain.com',
  process.env.VAPID_PUBLIC_KEY,
  process.env.VAPID_PRIVATE_KEY
);

async function sendAlert(sub, story) {
  const payload = JSON.stringify({
    title: story.headline,                 // keep it short
    body: story.summary,
    data: { url: `/news/${story.slug}?src=push` }   // article fetched on click
  });
  const subscription = {
    endpoint: sub.endpoint,
    keys: { p256dh: sub.p256dh_key, auth: sub.auth_secret }
  };
  return webpush.sendNotification(subscription, payload, {
    TTL: 300,                  // 5 minutes — drop if not delivered promptly
    urgency: 'high',
    topic: `story-${story.id}` // collapse: a correction supersedes this alert
  });
}

A worker pulls a batch off the queue and sends concurrently within a rate budget:

async function processBatch(queue, story, concurrency = 200) {
  const batch = await queue.pull(concurrency);   // pull from the fan-out queue
  await Promise.allSettled(batch.map(async (sub) => {
    try {
      await sendAlert(sub, story);
    } catch (err) {
      if (err.statusCode === 410 || err.statusCode === 404) {
        await queue.prune(sub);                  // dead endpoint
      } else if (err.statusCode === 429 || err.statusCode >= 500) {
        await queue.requeueWithBackoff(sub);     // transient — retry later
      }
    }
  }));
}

Throughput math

Size the send tier from arithmetic, not optimism. Take a worked example: two million eligible subscriptions, an editorial target of “everyone within sixty seconds”, and a measured mean of 40 ms per HTTPS request to the push service including TLS resumption and response read. Two million divided by sixty is 33,333 sends per second. One process with a concurrency of 200 in-flight requests delivers 200 / 0.04 = 5,000 sends per second — a seven-minute fan-out, six minutes of which is a headline going stale. Four such workers reach 20,000/s; eight reach 40,000/s and clear the target with headroom for retries.

Worker count needed to hit a 60-second fan-out target Assuming two million subscriptions, a sixty second target and 40 ms mean per-send latency, one worker at 200 concurrency sustains 5,000 sends per second, four workers 20,000, and eight workers 40,000, which is the first configuration above the required 33,333 per second. 2M subscribers in 60 s needs 33,333 sends/s Configuration Sustained sends per second (200 in flight per worker, 40 ms mean) required 33,333/s 1 worker 5,000/s — 6.7 min fan-out 4 workers 20,000/s — 100 s fan-out 8 workers 40,000/s — 50 s fan-out, first config that clears the target Worked example only: substitute your own subscriber count, target window and measured per-send latency, then leave headroom for 429 retries.
Concurrency divided by mean request latency gives sends per second; everything else is how many workers you run.

Two things spoil that arithmetic in production. The first is connection setup: opening a fresh TLS connection per send can cost more than the request itself, so keep long-lived pooled connections per push service host — the mechanics are in HTTP/2 connection pooling for web push at scale, and the concurrency shape that actually pays off is measured in parallel vs sequential push sending benchmarks. The second is rate limiting: pushing 40,000/s at one endpoint is precisely the condition that produces 429 Too Many Requests, and a burst answered by dropping the remainder of the audience is a worse outcome than a slower fan-out. Treat the limit as part of the budget and requeue rejected sends per handling 429 Too Many Requests from push services.

Note also that an alert like this is unambiguously editorial rather than account-driven, so it is subject to whatever consent and quiet-hour rules you apply to campaign traffic; the boundary is drawn in transactional vs marketing push notifications. A subscriber who opted in to “world news” should not receive sports alerts because the fan-out query was cheaper to write without a topic filter.

Steps to architect breaking-news alerts

  1. Decouple trigger from delivery. An editorial action enqueues one fan-out job; it never sends inline.
  2. Fan out through a fast queue sized for burst load. Choose the backing store and partitioning per scaling push queues with Redis or RabbitMQ.
  3. Batch and parallelize sends across worker pools, tuning batch size and concurrency against push-service rate limits — see message batching and throughput optimization.
  4. Set headers per send: urgency: high, low TTL, shared topic key.
  5. Handle failures inline: prune 410 Gone, requeue 429/5xx with backoff so a rate-limit spike doesn’t drop the audience.
  6. Use the collapse key for corrections — reuse the same topic so an update replaces the undelivered original.
  7. Monitor time-to-deliver and CTR as the success metrics; for news, speed is the product.

Gotchas and edge cases

  • High TTL on a time-critical alert. A long TTL means a stale headline lands hours later as misinformation. Keep TTL to minutes so undelivered alerts expire.
  • No collapse key. Without a shared topic, a correction stacks on top of the wrong original. Reuse the story-scoped key so the latest supersedes the rest.
  • Sending inline from the trigger. Blocking the editorial action on millions of sends guarantees a timeout. Always enqueue and fan out asynchronously.
  • Ignoring 429 under burst. A breaking story is exactly when you’ll hit push-service rate limits. Requeue with backoff rather than dropping; see retry logic and backoff.
  • Fat payloads. Embedding article text blows past the 4 KB aes128gcm limit. Send a headline and deep link; fetch the story on click.

FAQ

What TTL should breaking-news alerts use?

A low TTL of roughly 60–600 seconds. The value of a news alert decays in minutes, so a long TTL just delivers a stale headline late, which reads as misinformation. A short TTL tells the push service to discard any alert it can’t deliver promptly, which is the correct behavior for time-critical news.

How do collapse keys help with breaking news?

A shared topic/collapse key lets a follow-up or correction replace an undelivered original carrying the same key. A device that was offline and reconnects then receives only the latest version of the story rather than a stack of superseded alerts, which keeps corrections accurate and avoids notification spam.

What's the main bottleneck in breaking-news delivery?

Fan-out throughput. A single story can require millions of sends within seconds, so the constraint is queueing and send concurrency against push-service rate limits, not payload size. The architecture decouples the trigger from delivery via a fast queue feeding batched, rate-limited worker pools.