Designing a Message Forward / Fanout System
A complete, ground-up walkthrough of how WhatsApp, Slack, Discord, Twitter/X and every large chat platform take a single message from a single sender and deliver it — reliably, in order and at low latency — to millions of recipients across devices, networks and continents.
The Big Idea, in One Breath
A message forward / fanout system is the machinery that takes one incoming write — a chat message, a post, an event, a notification — and duplicates it into every place where it needs to appear: N recipients’ inboxes, M devices per user, a search index, a moderation queue, a push-notification service, and often an audit log. It must do that quickly, in order per conversation, without losing or duplicating a single copy, even when networks are slow and downstreams occasionally fail.
The tricky part is not the copying — it is the economics of it. Fanning out one message to ten friends is trivial. Fanning out one message from a celebrity to fifty million followers, while the same platform is doing that for a hundred other celebrities in the same second, is one of the classic hard problems of internet-scale software.
Picture a newsroom the moment a big story breaks. One reporter files a single story. Within minutes, that same story must reach the front page, the mobile app, the newsletter, the wire services, the search index, the archive and every subscriber’s home. Each channel has different urgency, format and reliability needs, but every copy must be the same story, in the right order, without the newsroom itself grinding to a halt. A fanout system is that newsroom — but running billions of times a day, silently, in software.
per message
to the last recipient
per subscriber
What Fanout Really Is
Before we design one, we need to pin down exactly what the system does — and what it does not. Fanout is not caching, not queuing, not pub/sub on its own, and not push notifications. It is the layer that owns the question “who should see this, and how do we get it to them?”
2.1 A Working Definition
A message forward / fanout system is a distributed component that, given a single accepted write (message, post, event) and a set of subscribers (recipients, followers, channel members, downstream services), guarantees:
- each subscriber receives a copy in a bounded time window,
- each subscriber receives it at-least-once and can deduplicate to exactly-once,
- ordering is preserved per conversation / topic / partition key (not necessarily globally),
- the system is able to keep up during viral traffic without collapsing back onto the origin write path, and
- every delivery is observable, replayable and auditable.
2.2 Where You Encounter It
1:1 & Group Messaging
WhatsApp, iMessage, Signal, Telegram — every message is fanned out to the other participants’ devices and their offline mailboxes.
Social Feeds & Timelines
Twitter/X, Instagram, TikTok, LinkedIn — a post is fanned out into millions of follower home timelines (fanout-on-write) or served from the author’s outbox (fanout-on-read).
Team Collaboration
Slack, Teams, Discord — a channel message is fanned out to every online member and buffered for those who are offline.
Event Bus & Webhooks
An order.placed event is forwarded to inventory, billing, fraud, analytics, search and 20 other downstreams — each with its own SLA and failure model.
2.3 What It Is Not
Fanout is not the same as a message broker, a push-notification service or an inbox database — even though it uses all three. It is the coordination layer that decides who, when, how and with what guarantees, and delegates the actual byte-moving to those specialised services.
The broker is a pipe. The push service is a shout. The inbox is a mailbox. Fanout is the postmaster: it looks at the envelope, decides which pipes to use, which mailboxes to fill, which shouts to make, and keeps a record of every copy sent so nothing is lost and nothing is delivered twice.
Why It Matters So Much
Fanout is one of the most quietly load-bearing systems in modern software. Every social feed, every chat app, every event-driven platform depends on it. When it works, nobody notices. When it breaks, users see missing messages, out-of-order threads and stale timelines — and the platform’s brand takes the hit.
3.1 The Business & Human Problem
- Trust in real-time. Users assume “send” means “delivered.” A single missing message erodes trust faster than any UI polish can rebuild it.
- Cost of write amplification. One popular post can create tens of millions of writes. Naively fanning out on the write path can quickly bankrupt the storage tier.
- Cost of read amplification. Naively fanning out on the read path can burn CPU on every timeline load and turn a hot celebrity into a hot key.
- Regulatory audit. In finance, health and safety domains, every downstream copy must be traceable to the originating event, including retries and failures.
- Operational sanity. Fanout is where blast radius lives. A single misconfigured downstream can turn a healthy system into a global incident within minutes if fanout is unbounded.
3.2 What Makes It Uniquely Hard
Harder than pub/sub
- Subscriber sets are dynamic, per-message and enormous.
- Per-recipient state (last-read cursor, mute, block, do-not-disturb) has to be respected.
- Ordering per conversation must survive retries and failovers.
Harder than queueing
- Fanout is a distribution problem, not a single-line problem.
- Hot fanout keys (celebrities, popular channels) break naive sharding.
- Delivery guarantees must survive downstream partial outages.
A fanout system exists to make one small truth — “this message was sent” — instantly, silently and reliably true in a million other places at once. Every design decision in this chapter serves that quiet, load-bearing promise.
The Building Blocks
A production fanout system is a small constellation of focused services. Each has one narrow job; the leverage is in how they compose.
Ingress / Write API
The single, authenticated entry point for new messages or events. Validates, assigns IDs, appends to the write-ahead log, and returns quickly.
Write-Ahead Log
Durable, ordered log (Kafka, Pulsar, Kinesis) that is the authoritative source of truth. Fanout workers consume from here, never from the user API.
Subscriber Resolver
Given a message, returns the set of recipients: chat participants, channel members, followers, downstream services. Cached, sharded, and versioned.
Fanout Planner
Decides how to fan out this specific message: fanout-on-write, fanout-on-read, hybrid, or async batch — based on subscriber-set size and hotness.
Delivery Workers
Stateless workers that read plans from the queue and push copies into the correct sinks (inbox DB, push service, websocket gateway, external webhook).
Per-User Inbox Store
Ordered, append-only per-recipient inbox for offline / historical access. Sharded by userId. Reads support pagination and sync.
Real-Time Push Gateway
WebSocket / MQTT / HTTP-push fleet that maintains persistent connections and delivers messages to online devices with sub-second latency.
Presence & Device Registry
Tracks which devices a user has, which are online, and which push tokens are valid. Consumed by the planner to skip dead endpoints.
Dedup / Idempotency Store
Records (messageId, subscriberId) pairs already delivered. Guarantees at-most-once effect even when at-least-once delivery is used.
Retry & Dead-Letter Queue
Handles downstream failures with exponential backoff, isolates poison messages to a DLQ, and provides an operator UI for replay.
Policy & Fairness Layer
Rate limits, per-tenant fairness, do-not-disturb rules, mute/block enforcement, geographic residency constraints.
Observability
Metrics (fanout-out ratio, delivery latency, retry rate, DLQ depth), traces per messageId, dashboards per tenant / channel / region.
Fanout Strategies: On-Write, On-Read, Hybrid
The single most consequential design decision is when the copy is made. There are three canonical strategies, and mature systems use all three — but for different traffic shapes.
5.1 Fanout-on-Write (Push Model)
At write time, the system materialises the message into every recipient’s inbox. Reads are then cheap and O(1). Great for chats, group messaging and follower counts in the thousands.
- Pros: Fast reads, simple timeline logic, easy per-user personalisation.
- Cons: Write amplification proportional to fanout size. A 50M-follower post produces 50M inbox writes.
- Fits: 1:1 chat, small groups, low-fanout feeds.
5.2 Fanout-on-Read (Pull Model)
At write time, the message is stored only in the author’s outbox. At read time, the recipient’s timeline is composed by fetching from the outboxes of everyone they follow.
- Pros: Constant, tiny write cost regardless of how many followers.
- Cons: Reads become expensive; timeline composition on every scroll; hard to personalise cheaply.
- Fits: Very-large-fanout accounts (celebrities), low-read-frequency users.
5.3 Hybrid Fanout
The system classifies producers and recipients by hotness and picks a strategy per pair:
function decide_fanout(msg, author, subscribers):
if subscribers.count <= SMALL_THRESHOLD: # e.g. 10_000
return "fanout_on_write"
if author.is_hot_producer(): # celebrity
return "fanout_on_read" # merge at read time
# medium producers: split
hot_subs = subscribers.filter(is_active_this_hour)
cold_subs = subscribers - hot_subs
return {
"hot" : { "strategy": "fanout_on_write", "targets": hot_subs },
"cold": { "strategy": "fanout_on_read", "targets": cold_subs }
}5.4 Comparison at a Glance
| Strategy | Write cost | Read cost | When it shines |
|---|---|---|---|
| On-write (push) | O(N subscribers) | O(1) | Chats, small groups, low fanout |
| On-read (pull) | O(1) | O(K followees) | Celebrities, low-read users |
| Hybrid | Bounded | Bounded | Real social platforms at scale |
| Async batch | Deferred | Delayed | Non-real-time notifications, digests |
If your fanout size looks like a straight line, use on-write. If it looks like a power-law with a fat tail, use hybrid and put the tail on on-read. Never let a single celebrity decide the write budget for the whole platform.
Delivery Patterns: Order, Dedup and Guarantees
Deciding who is only half the story. The other half is in what order, with what guarantees, and how to survive downstream flakiness. Get these wrong and users see interleaved threads, duplicate notifications, and phantom messages.
6.1 Ordering: Global Is Fiction, Per-Key Is Truth
Global ordering across billions of messages is neither achievable nor useful. What users actually notice is ordering within a conversation, channel or topic. So the system:
- partitions the write-ahead log by
conversationId/channelId/topicKey, - ensures each partition is consumed by exactly one worker at a time,
- and preserves that partition’s order through retries via idempotent, monotonically increasing sequence numbers.
6.2 Delivery Semantics
| Semantics | What it means | Cost |
|---|---|---|
| At-most-once | Fire and forget; may lose messages | Cheapest; almost never acceptable |
| At-least-once | Guaranteed delivery; possible duplicates | Default choice; needs consumer dedup |
| Exactly-once (effective) | At-least-once + idempotency store | Higher latency & storage; user-facing correctness |
Real systems almost always ship at-least-once on the wire and rely on the dedup store to produce exactly-once effects. Insisting on true exactly-once end-to-end usually costs more than it delivers.
6.3 Idempotency Keys
Every delivery attempt carries a stable key: (messageId, subscriberId). The delivery worker checks the dedup store before applying the effect. Keys are TTL-bound to keep storage bounded; the TTL is chosen to exceed the worst-case retry window.
6.4 Backpressure & Retry
Backpressure
- Delivery workers must slow ingestion when downstreams are struggling, not overrun them.
- Queue depth, not CPU, is the right auto-scale signal.
Retry & DLQ
- Exponential backoff with jitter for transient failures.
- Poison messages go to a dead-letter queue with reason codes for manual replay.
A common bug: retries succeed but the ack path fails, and the same message gets re-delivered forever. Solution: acknowledge before attempting the side-effect only when the effect is safe on replay, and always store the dedup key first when it is not.
Streaming vs Batch, Monolith vs Distributed
Two architectural axes must be picked early. Both decisions have long, expensive tails.
7.1 When Does Fanout Fire?
- Streaming — every accepted write triggers immediate fanout. Required for chat and real-time feeds.
- Micro-batching — group messages by conversation for a few tens of milliseconds and fan out in one pass. Great for high-fanout collaborative apps.
- Batch / digest — deferred fanout used for email digests, weekly newsletters and low-priority notifications.
7.2 Deployment Shape
| Shape | When it fits | Trade-offs |
|---|---|---|
| Monolith service | Small startup, single tenant, < 1k msg/s | Fast to build; caps at one machine’s throughput |
| Stream processor (Flink / Kafka Streams) | > 10k msg/s, per-conversation state, multi-tenant | Great throughput; ops-heavier; exactly-once by construction |
| Actor / stateful service (Orleans, Akka) | Long-lived conversations with rich state (typing, presence) | Clean per-conversation model; needs sharding & failover |
| Serverless functions | Very bursty, low sustained traffic (webhooks) | Cheap at rest; cold starts hurt sub-second SLAs |
7.3 Sharding Strategy
The natural unit is the conversation / channel / topic. The write-ahead log is keyed by that identifier, and each partition is consumed by exactly one worker. Delivery workers are stateless and horizontal — they can be scaled independently of the log.
7.4 Hot Fanout Keys
A single celebrity account, a viral incident channel, or a “#general” in a 100k-employee Slack can saturate one partition. Techniques that help:
- Sub-key splitting: split a hot key into
Nshards (channel:123#0..N) and reassemble in order downstream. - Fanout tiering: put hot producers on their own worker pool with priority quotas.
- Sampling low-value events (typing indicators, cursor updates) during peaks.
A viral event can 100× one partition’s traffic in seconds. Auto-scale on partition lag, not global CPU, and pre-warm delivery workers before predictable peaks (news events, launches, Super Bowl).
End-to-End Flow: One Message’s Life
Enough abstraction. Let us follow one message — a group chat post to 800 people — from “Send” on the sender’s phone to the last recipient’s notification.
Ingress accepts the write
Client posts to /v1/messages with an idempotency key. Ingress validates auth, assigns messageId=m_9f2a, appends to the write-ahead log partition for conversationId=c_44e1, and returns 202 Accepted.
Fanout planner picks a strategy
A worker consuming partition c_44e1 pulls the message. Subscriber resolver returns 800 participants. Planner picks fanout-on-write (bounded size, low producer hotness).
Plan is split into delivery tasks
800 delivery tasks are enqueued, keyed by (messageId, userId). Each carries the payload, dedup key, priority and target sinks (inbox + push + search).
Delivery workers run in parallel
Stateless workers pull tasks. For each user they: (a) idempotency check, (b) append to per-user inbox with the conversation’s sequence number, (c) look up presence.
Online devices receive over WebSocket
For each user marked online, the push gateway delivers the payload over their persistent socket. Median latency to online users: ~200 ms end-to-end.
Offline devices get push notifications
For offline users with valid push tokens, the worker calls APNs / FCM. If tokens are dead, presence & device registry is updated and the message stays in the inbox for next login.
Downstream sinks receive their copies
Search index consumes the same log for full-text search. Analytics gets an event. Moderation queue gets a copy if content flags trigger.
Retries, DLQ & observability
A handful of tasks fail on flaky mobile push. Exponential backoff kicks in; three retries land within the SLA. A single poison payload lands in DLQ with a reason code for the on-call to inspect.
Quality Attributes: The “-ilities”
The non-functional targets for a fanout system are unusual: it must be simultaneously fast, correct, elastic and cheap — and be all four during the worst hour of the year.
Delivery Latency
Median < 500 ms to the last online recipient; P95 < 2 s. Offline delivery through push completes within seconds after presence returns.
Throughput
Sized for peak: millions of ingested writes/second globally, tens of millions of delivered copies/second at peak fanout.
Ordering
Per-conversation ordering is inviolable. Global ordering is not attempted; the API contract makes that explicit.
Delivery Semantics
At-least-once on the wire, exactly-once effect via idempotency store. Zero silent drops.
Reliability
Downstream outages are absorbed by retry + DLQ. A dead push provider does not break the inbox path.
Scalability
Sharded by conversation. Delivery workers scale on partition lag; hot keys are sub-sharded automatically.
Availability
Graceful degradation: if the push gateway is down, inbox is still updated; when it comes back, catch-up delivers the tail.
Observability
Every messageId is traceable through ingress, log, planner, worker, sink. Fanout-out ratio and DLQ depth are top-line SLOs.
9.1 The Latency Budget
| Hop | Target | How |
|---|---|---|
| Ingress accept | < 30 ms | Warm workers, in-VPC log append with quorum ack |
| Log → worker | < 50 ms | Kafka in-region, tight commit intervals |
| Resolver + planner | < 30 ms | Cached subscriber sets, in-memory decision table |
| Delivery worker per recipient | < 40 ms | Batched idempotency check + inbox append |
| Push gateway to online device | < 100 ms | Persistent socket, pre-serialised payload |
| Total to last online recipient | ~400–500 ms P95 | Within perception of “instant” for chat/feed |
Common Pitfalls & Trade-offs
Every fanout system, in production, gets bitten by the same handful of subtle bugs. Knowing them turns quarters of firefighting into a paragraph in a design review.
10.1 Ten Traps We’ve All Fallen Into
Hot celebrity kills the write path
One 50M-follower post takes down inbox writes for everyone else. Split producers by hotness; put celebrities on the pull path.
No idempotency key
Client retries create duplicate messages. Every write carries a client-supplied idempotency key; the API dedups server-side.
Ack before durable
Ingress acknowledges before the log append is quorum-safe. A single broker failure loses messages. Always ack after log durability.
Global ordering promised
API doc says “messages arrive in order.” They do — per conversation. Users then discover cross-conversation interleaving is not ordered. Document the guarantee precisely.
Push token graveyard
Dead APNs / FCM tokens are retried forever. Presence & device registry must age out tokens on hard failures.
Retry storm on downstream outage
Single sink flakes; retries pile up and take out neighbours. Circuit-breaker per sink; shed load or park the queue rather than hammer.
DLQ nobody reads
Messages quietly land in DLQ for months. Add SLOs on DLQ depth and an on-call rotation for replay/discard decisions.
Multi-tenant unfairness
One noisy tenant hogs all delivery workers. Fair-share scheduling with per-tenant quotas.
Subscriber-set staleness
Someone gets kicked from a group but keeps receiving messages for an hour. Version subscriber sets and honour the version in the delivery task.
No shadow mode for new sinks
Ship a new downstream, it collapses under real fanout traffic. Every new sink runs in shadow with mirrored traffic first.
10.2 The Trade-offs You Cannot Avoid
Push vs Pull
- Push (on-write) is fast to read, expensive to write for high fanout.
- Pull (on-read) is cheap to write, expensive to read on scroll.
- Hybrid, with a per-user or per-pair decision, is the only strategy that scales beyond a few million users.
Consistency vs Latency
- Strict per-recipient consistency across devices is expensive.
- Best-effort per-device with a strongly consistent inbox is the pragmatic compromise.
Context
We must decide the delivery semantics guaranteed to clients and downstream systems.
Decision
Adopt at-least-once wire delivery with server-side idempotency keyed on (messageId, subscriberId). Every message carries a durable dedup key, TTL-bound to cover the worst-case retry window. The public contract is “exactly-once effect, at-least-once attempt.”
Consequences
Higher storage cost for the dedup store, but bounded and predictable. Simpler retry logic on every downstream. No user-visible duplicates in normal or degraded operation.
How Fanout Systems Evolve
Fanout is not new — every distributed system has had to solve some version of it. What has changed is the scale, the latency expectations and the number of downstreams. Understanding the waves helps place your own platform.
Wave 1 — Single Broker (pre-2010)
ActiveMQ / RabbitMQ with topic-based routing. Great for internal event buses; wilts under celebrity fanout.
Wave 2 — Push Timelines (2010–2013)
Early Twitter, Facebook News Feed — fanout-on-write to materialised inboxes. Explodes at ten-million-follower accounts.
Wave 3 — Hybrid Fanout (2013–2018)
Push for the many, pull for the few, mixed at read time. Kafka becomes the shared log. Slack, WhatsApp and Instagram grow up here.
Wave 4 — Multi-Tenant, Multi-Sink (2018–2023)
Every message must land in inbox, search, analytics, moderation, audit and third-party webhooks. Sink isolation and fair-share scheduling become first-class.
Wave 5 — Edge Fanout & AI-Aware (2024+)
Delivery closer to the user with regional edges; presence-aware, personalised prioritisation; AI-generated summaries and translations produced as part of the fanout pipeline.
11.1 Adjacent Systems That Plug In
Search Index
Consumes the fanout log as its input; guarantees search consistency with delivery.
Moderation Pipeline
Flagged content forks off the fanout with high priority to human review before broad delivery.
Push Providers
APNs, FCM, WNS, WebPush; each with its own quirks, TTLs and rate limits.
Audit & Compliance
Every fanned-out copy is recorded with the producing policy version, retained per jurisdiction.
Key Takeaways
Fanout is a quiet, load-bearing layer. It never lands on a keynote slide, but it decides whether a chat feels “instant,” whether a feed feels “alive,” and whether a viral moment turns into an outage or into growth.
Key Takeaways
- Fanout is a distribution problem, not a queueing problem. Its job is to answer “who, when, how” and delegate the byte-moving to specialised sinks.
- The write-ahead log is the source of truth. Everything else is a materialised view; if the log is safe, the system can be rebuilt.
- Choose a strategy per pair, not per system. On-write for small fanouts, on-read for the fat tail, hybrid for the middle.
- Order per conversation, not globally. Document the guarantee precisely; global ordering promises will haunt you.
- At-least-once on the wire, exactly-once in effect. Idempotency keys are the compound-interest asset of the whole platform.
- Isolate hot keys early. Split, tier, sample — whatever it takes to stop celebrities from taxing every write.
- Backpressure is a feature. Auto-scale on partition lag; never let retries take down neighbours.
- Every downstream is untrusted. Circuit-breakers, per-sink quotas and a well-tended DLQ separate a resilient platform from a fragile one.
- Fair-share scheduling. A single noisy tenant must not consume the whole delivery capacity of the fleet.
- Shadow mode for every new sink and every new strategy. Real fanout traffic is the only honest load test.
The best fanout systems are the ones nobody talks about. When they work, users think “of course my message showed up on all my devices, that’s obvious.” When they fail, the platform is on the front page. Every design choice — log, planner, workers, sinks, dedup — is in service of that quiet, load-bearing obviousness.