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