Design WhatsApp / Chat System
hardA chat app has to deliver a message in under a second, to a phone that may be asleep, behind a patchy network, or switched off.
Key Concepts
High-level design
Connection: long-lived WebSocket/XMPP to gateway; sticky by user_id.
Send: client → gateway → message service → persist (sender outbox + recipient inbox) → presence → online? push to gateway : queue push notification.
Receive: client opens socket → server delivers buffered messages → client acks → server marks delivered.
Groups: fanout to per-recipient inboxes (bounded by group size cap).
E2EE: client-side encryption with Signal Protocol; server stores ciphertext only.
Media: encrypted blobs in S3-class storage.
Components
- WebSocket gateway (stateful; user → connection routing). Erlang/Elixir famous fit; Go and Rust also common.
- Presence service (Redis with TTL heartbeat).
- Message service + per-user inbox (Cassandra wide row by user_id, clustered by timestamp).
- Push gateway (APNs, FCM with batching and retry).
- Media service (encrypted blobs in S3 + CDN for distribution).
- Group metadata service (members, admins, settings).
- E2EE key server (prekey bundles, identity keys, signed prekeys for X3DH).
- Anti-abuse service (spam, content moderation if not E2EE).
Capacity, worked
Monthly users 2,000,000,000 Concurrent fraction at peak 25% Messages per user per day 40 ----------------------------------------------------------- Concurrent connections 2B x 0.25 = 500,000,000 Connections per tuned box 100,000 Connection servers 500M / 100k = 5,000
Messages per day 2B x 40 = 80,000,000,000 Messages per second 80B / 86,400 = 926,000 With sent/delivered/read (x3) = 2,800,000 At 3x peak = 8,300,000
Per-connection memory
Socket buffers + app state ~10-40 KB
100,000 x 25 KB = 2.5 GB/box Session registry
Entry: user id 8 B + server id 4 B + timestamp 8 B = 20 B
500M x 20 B = 10 GBConsequences:
- 5,000 servers exist before any feature does; connection handling is the
budget.
- 10 GB of registry fits in memory, so routing is a hash lookup.
- 8.3M frames/s at peak is why receipts are aggregated in large groups.
Connection and routing path
Connect
Device -> load balancer (TCP/TLS passthrough) -> connection server.
Server writes {user -> self} into the session registry with a TTL.
Heartbeats refresh both the socket and the registry entry. Send A -> B
1. A's server validates and assigns a per-conversation sequence number.
2. Persists to the recipient's queue (durability before acknowledgement:
acknowledge only once it is stored, or a crash loses the message).
3. Looks up B in the registry.
4. Online: forward to B's server, which writes to the socket.
Offline: leave it queued; a push notification wakes the device.
5. B's client acknowledges; a delivery receipt flows back to A. Disconnect
Registry entry expires by TTL rather than requiring a clean close --
phones lose signal without saying goodbye. Why the registry tolerates staleness
A stale entry costs one forward to the wrong server and a retry. Making
it strongly consistent would put a consensus round trip on every
message. The cheap failure is the right trade.Failure modes in production
Thundering herd on reconnect
Symptom: a regional network blip drops 10M sockets; reconnects arrive
together and take down the connection tier.
Cause: clients retry immediately and in lockstep.
Fix: exponential backoff with jitter on the client, and connection
admission limits per server.
Registry entry outlives the server
Symptom: messages forwarded into a black hole after a node dies.
Cause: entries removed only on clean disconnect.
Fix: short TTL refreshed by heartbeat; forwarding failure triggers a
re-lookup and, failing that, the offline queue. Acknowledgement before durability
Symptom: senders see one tick, the message never arrives after a crash.
Cause: acknowledging on receipt rather than after persistence.
Fix: acknowledge after the write; it costs a few milliseconds and
removes a whole class of "it said delivered" support tickets. Receipt storm in large groups
Symptom: receipt traffic exceeds message traffic; battery complaints.
Cause: per-member read receipts fanned out to every member.
Fix: aggregate ("read by 47"), or drop per-member receipts above a
group size and say so in the product.