SYSTEM DESIGN · DESIGN PROBLEMS · CHAT AND REAL TIME · MEDIUM · 5 OF 41

Design WhatsApp: One-to-One and Group Chat for Half a Billion Daily Users

A LADDER walkthrough of a mobile chat system: requirements, scale math for 20 billion messages a day and 200 million open connections, the WebSocket and REST API, a message store partitioned by conversation, then the deep dives: routing between chat servers, offline delivery and push, sent-delivered-read receipts, per-conversation ordering, group fan-out, presence, media and end-to-end encryption.

Reported atMicrosoftMetaAppleUberAirbnb

Try it on the practice board Drag the blocks, wire them up, and check your design against the reference.

  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
  6. 6
  7. 7
  8. 8
  9. 9
  10. 10
  11. 11
  12. 12
  13. 13
  14. 14
  15. 15
  16. 16
  17. 17
  18. 18
  19. 19
  20. 20
  21. 21
  22. 22
  23. 23
  24. 24
  25. 25
  26. 26
  27. 27
  28. 28
  29. 29
  30. 30
  31. 31
  32. 32
  33. 33
  34. 34
  35. 35
  36. 36
  37. 37
  38. 38
  39. 39
  40. 40
  41. 41
1 / 19

Press play to hear the narration, or step through at your own pace.

Notes

Listen

Listen. One-to-one messages, starting with text. Groups of up to about a thousand members. Sent, delivered and read receipts. If the recipient is offline, they get everything when they reconnect. Online status and last seen. Photos and voice notes. Non-functional: delivery in under half a second when both are online, messages are never lost, never duplicated, and appear in the order they were sent.

Scope it. Calls, stories and payments are out. Ask the questions that shape the storage. Does the server keep history forever? No: it keeps a message until it is delivered, and drops undelivered ones after thirty days. Is it end-to-end encrypted? Yes, so the server stores and relays ciphertext it cannot read, which rules out server-side search. Multiple devices per user we treat as an extension.

Requirements
  • functional:
  • 1:1 messages, text first
  • groups up to 1,024 members
  • sent, delivered, read ticks
  • offline users get messages
  • when they reconnect
  • online and last seen
  • photos and voice notes
  • non-functional:
  • under 500 ms when online
  • never lose, never duplicate
Scope and questions
  • out of scope: voice and video
  • calls, stories, payments,
  • contact discovery, spam
  • ask: keep history on server?
  • → only until delivered;
  • undelivered dropped at 30 d
  • ask: end-to-end encrypted?
  • → yes, server sees ciphertext
  • ask: multiple devices? → later

Ask scale

Ask about scale. Five hundred million daily users sending forty messages a day is twenty billion messages. Divide by a hundred thousand seconds: two hundred thousand messages a second, around six hundred thousand at peak. Every message also produces a delivered and a read receipt, so the event rate is roughly three times that. This is write-heavy, unlike the warm-ups.

Storage. Around two hundred bytes per message with metadata is four terabytes a day, at most about a hundred twenty terabytes for thirty days, before replication, since delivered messages are deleted. Media is far bigger, hundreds of terabytes a day, so it goes to blob storage, never the message store. Connections: if forty percent of users are online at peak, that is two hundred million open sockets; at half a million per server, about four hundred chat servers.

Traffic
500M daily active users
× 40 messages sent per day
= 20B messages/day
  / 10^5 s ≈ 200,000 msgs/s
  peak ×3 ≈ 600,000 msgs/s

each message also creates
  2 receipts (delivered, read)
  → ~1.8M events/s at peak

write-heavy and fan-out heavy
Storage and connections
message ≈ 200 B with metadata
20B × 200 B = 4 TB/day
  × 30 days ≈ 120 TB (cap)
  × 3 replicas ≈ 360 TB

media: 2B items × 200 KB
  = 400 TB/day → blob store

concurrent: ~40% online
  ≈ 200M open connections
  / 500k per server ≈ 400 boxes

Define API

Define the API. Real-time traffic rides a WebSocket. The client sends a message with its own client message ID, the chat_id and ciphertext. The server acks with that ID and the assigned sequence number. The server pushes messages and receipts the same way. Over plain HTTP: a sync endpoint that returns everything since a cursor, for reconnects, and an endpoint that hands out media upload URLs.

API
WebSocket frames:
  send { client_msg_id, chat_id,
         ciphertext }
  ack  { client_msg_id, seq }
  msg  { chat_id, seq, from,
         ciphertext }
  receipt { chat_id, seq,
            delivered | read }
REST:
  GET /sync?since=cursor
  POST /media → upload URL

Data model

The data model. Messages go in a wide-column store like Cassandra, partitioned by chat_id and clustered by sequence number, so reading a conversation in order is one partition scan. It handles huge write volume and supports a thirty-day TTL natively. A per-recipient inbox lists undelivered messages, so reconnect sync is a single partition read. A small chats table tracks members and the last sequence number.

Data model
messages  (Cassandra)
  PRIMARY KEY ((chat_id),
               seq)
  from, ciphertext, sent_at
  TTL 30 days

inbox  (per recipient)
  PRIMARY KEY ((user_id), ts)
  chat_id, seq  (undelivered)

chats: members, last_seq

High-level design

Draw the high-level design, starting with connections. Each phone opens one long-lived WebSocket through a layer four load balancer to one of the chat servers. After authenticating, the chat server records in the session registry that user B is connected to chat server two twelve, with a short expiry refreshed by heartbeats. The registry is a sharded in-memory store like Redis.

Now trace a message. User A's chat server receives it, assigns the next sequence number for the conversation, and appends it to the message store. Only after the write is durable does it ack user A, and their phone shows one tick. Then it looks up user B in the registry, forwards the message to B's chat server, and that server pushes it down B's socket. Store first, then deliver: a crash can delay a message but never lose it.

Deep dive: routing

Deep dive one: routing. Two hundred million registry entries at about fifty bytes is around ten gigabytes, easy for a Redis cluster. The sender's chat server calls the recipient's server directly over gRPC. If the entry is stale because a server died, the call fails and we treat the user as offline. Nothing is lost, because the message is already in the store. Pub-sub per user is an alternative; see the Real-time updates pattern.

Routing between servers
  • registry: user → server,
  • TTL 60 s, renewed by
  • heartbeat every 30 s
  • 200M × 50 B ≈ 10 GB in Redis
  • sender server → recipient
  • server over gRPC
  • stale entry (server died)?
  • → call fails, treat user as
  • offline, message is already
  • in the store

Deep dive: offline

Deep dive two: offline users. If user B is not connected, the message stays in B's inbox and the push gateway sends a notification through Apple or Google push, which wakes the phone without exposing the content. When B reconnects, the app calls sync with its last cursor and receives everything it missed. Once the phone acks, the server deletes the inbox entries and sends user A a delivered receipt.

Offline delivery
  • message stays in B’s inbox
  • push gateway → APNs / FCM
  • (wakes the phone; payload
  • has no plaintext)
  • B reconnects: GET /sync
  • since its cursor
  • server returns the inbox,
  • B acks, server sends
  • "delivered" to A

Deep dive: receipts

Deep dive three: receipts and duplicates. One tick means the server stored the message. Two ticks means B's device acknowledged it. Blue means B opened the chat, which sends a read receipt back like a tiny message. Networks drop acks, so a phone resends a message it never got an ack for, with the same client message ID. The server dedupes on sender and that ID. At-least-once delivery plus dedupe looks exactly once to the user.

Ticks and exactly once
  • one tick: server stored it
  • two ticks: recipient device
  • acked it (delivered)
  • blue: recipient opened it
  • (read receipt)
  • retries: client resends with
  • the same client_msg_id
  • server dedupes on
  • (sender, client_msg_id)
  • → at least once + dedupe

Deep dive: ordering

Deep dive four: ordering. You do not need a global order, only order within each conversation. Assign a sequence number per chat from a single owner, either by routing each chat_id to one server, or with a conditional update on the chat's last sequence. Phone clocks are unreliable, so never sort by device time. The client sorts by sequence, and if number five hundred arrives before four ninety-nine, it waits briefly for the gap to fill.

Ordering
  • global order is impossible and
  • unnecessary
  • order per conversation:
  • the owning server assigns
  • seq = last_seq + 1
  • (one writer per chat_id,
  • or a conditional update)
  • client sorts by seq, shows a
  • gap until the missing one
  • arrives

Deep dive: groups

Deep dive five: groups. Store a group message once, in the group's partition, then fan out to members: online members get it pushed through their chat servers, offline members get an inbox entry and a notification. Capping groups at about a thousand members keeps fan-out bounded. If the average message reaches five recipients, two hundred thousand messages a second become a million deliveries a second, which is why delivery runs through queues in each chat server.

Group fan-out
store the message once:
  partition chat_id = group 77
fan out to 250 members:
  online → push via their
    chat server
  offline → inbox row + push

cap at 1,024 members keeps
  fan-out bounded
200k msgs/s × avg 5 recipients
  ≈ 1M deliveries/s

Deep dive: presence

Deep dive six: presence. Online means the user has a live socket; last seen is the last heartbeat, written at most once a minute to keep writes down. The trap is broadcasting every status change to every contact: a user with five hundred contacts flapping between online and offline creates a storm. Instead, subscribe to someone's presence only while their chat is open, and wait a few seconds before declaring someone offline.

Presence
  • online = has a live socket
  • last seen = last heartbeat,
  • written at most once a min
  • do not broadcast to every
  • contact on every change
  • subscribe only while a chat
  • with that person is open
  • debounce flapping networks
  • (wait ~10 s before offline)

Deep dive: media

Deep dive seven: media and encryption. The phone encrypts the photo with a random key, uploads it to blob storage through a presigned URL, and sends a normal message containing the media ID and that key, all end-to-end encrypted. Recipients download the blob directly and decrypt it. The servers only ever handle ciphertext. The trade-off is real: no server-side search, and no content scanning. See the Large blobs pattern.

Media and encryption
  • client encrypts the photo
  • with a random key
  • uploads to blob storage via a
  • presigned URL
  • message carries media id +
  • key, itself encrypted
  • recipients download directly
  • server never sees plaintext:
  • no server-side search or
  • content scanning

Trade-offs

Trade-offs and failures. When a chat server dies, half a million phones reconnect, so clients use jittered backoff and then sync, which makes the outage invisible apart from a short delay. If the store is slow, the server delays acks, and phones simply retry. Monitor send-to-delivery latency at p ninety-nine, inbox depth, reconnect rate and push errors. At ten times the scale, add chat servers and message store nodes; the design is horizontally sharded throughout.

Trade-offs and failures
  • chat server dies → 500k
  • phones reconnect with
  • jittered backoff, then sync
  • store slow → hold acks; no
  • ack means the phone retries
  • hot group → bounded size,
  • per-server batching
  • monitor: send-to-deliver p99,
  • undelivered inbox depth,
  • reconnect rate, push errors
  • 10x: more chat servers, shards

Interview summary

The summary. Phones keep a WebSocket to chat servers, and a registry maps each user to their server. A message is stored first, in Cassandra by chat and sequence, then acked. If the recipient is online it is forwarded; if not, it waits in their inbox with a push notification and is synced on reconnect. Client message IDs dedupe retries, per-chat sequences give order, groups store once and fan out, and the server relays only ciphertext.

Interview summary
  • WebSocket to chat servers,
  • registry: user → server
  • store first (Cassandra by
  • chat_id, seq), then ack
  • recipient online: forward;
  • offline: inbox + push + sync
  • client_msg_id dedupe
  • per-chat sequence numbers
  • groups: store once, fan out
  • E2E: server relays ciphertext

Interview questions

How does a message reach a user connected to a different chat server?

The sender’s chat server looks up the recipient in a session registry that maps users to servers and forwards the message to that server, which pushes it down the recipient’s WebSocket; if the lookup or call fails the user is treated as offline.

How do you deliver messages to users who are offline?

Persist the message, add it to the recipient’s inbox, send a push notification through APNs or FCM, and when the device reconnects it syncs everything after its last cursor and acknowledges it, triggering the delivered receipt.

How do you avoid duplicate messages when phones retry?

The client attaches a unique client message ID and resends with the same ID until it gets an ack; the server deduplicates on sender and client message ID, so at-least-once delivery appears exactly once.

How do you keep messages in order?

Order is only needed within a conversation, so a single owner assigns a monotonically increasing sequence number per chat; clients sort by that number rather than device timestamps and wait briefly to fill gaps.

Which database would you use for messages and why?

A wide-column store such as Cassandra, partitioned by chat ID and clustered by sequence: it handles hundreds of thousands of writes per second, reads a conversation in order from one partition, and supports TTLs for retention.

How do group messages work?

The message is stored once in the group’s partition, the group service supplies the member list, and the message is fanned out to each member’s chat server or inbox; a member cap keeps the fan-out bounded.

How would you implement online status without overwhelming the system?

Derive online status from live sockets and heartbeats, write last-seen at most periodically, and only push presence updates to users who currently have that contact’s chat open, with debouncing for flapping connections.

What does end-to-end encryption change in the design?

Servers store and relay only ciphertext and encrypted media keys, so they cannot search, index or moderate message content, and key distribution and multi-device support become client responsibilities.