Design WhatsApp

HardMessaging & CollaborationWebSocketsEnd-to-End EncryptionMessage QueuesDelivery Receipts

Problem

Design a mobile messaging app like WhatsApp. Users send text messages and media in one-to-one chats and in small groups of up to 256 members. Messages sent to an offline user are delivered when that user comes back online, and senders see sent, delivered, and read receipts. Messages are end-to-end encrypted, so the server can never read them. Users can also see whether a contact is online or when they were last seen. The service has 2 billion users, 500 million daily active users, and carries 100 billion messages per day.

  • Does the server keep message history after delivery, or only until every recipient device has the message?
  • Do we support multiple devices per user (phone plus desktop), and must each device decrypt independently?
  • What is the maximum media size, and is media also end-to-end encrypted?
  • How strict is ordering: per chat, or globally per user?
  • Should read receipts and last seen be controllable by privacy settings?
  • Are voice and video calls in scope, or only messaging?

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • Send and receive text and media messages in one-to-one chats.
  • Support group chats of up to 256 members.
  • Deliver messages to offline recipients when they reconnect.
  • Show sent, delivered, and read receipts to the sender.
  • Encrypt messages end to end so only participants can read them.
  • Show online status and last seen, subject to privacy settings.
  • Support up to four linked devices per user.

Items in bold are existing requirements. The rest are suggestions. Confirm with the interviewer whether they are relevant first.

  • 100 billion messages per day is about 1.2 million messages per second on average and 3 million at peak.
  • Delivery latency under 500 ms when both parties are online.
  • At-least-once delivery with deduplication on the device, so a message is never lost and never shown twice.
  • Messages within a chat appear in the order they were sent.
  • 500 million concurrent connections at peak; at 1 million connections per server, that is about 500 chat servers plus headroom.
  • Offline queue storage: messages wait at most 30 days; at 100 bytes of ciphertext per message and maybe 10% undelivered at any time, that is about 1 TB per day of transient data.
  • Media: if 5% of messages carry a 200 KB file, that is 1 PB of new media per day, served from blob storage and a CDN.

Each device keeps one persistent connection to a chat server. A sender's chat server looks up which server the recipient is connected to and forwards the encrypted message there, and if the recipient is offline the message waits in a per-device inbox until it reconnects and acknowledges it. The server only ever handles ciphertext: devices exchange keys through a key directory and encrypt with the Signal protocol, and group messages are encrypted once per group with a sender key and fanned out by the server.

Loading diagram...

Send a one-to-one message. The sender's device encrypts the message for each of the recipient's devices, attaches a client-generated message ID, and sends it over its connection. The chat server persists it to the recipient's per-device inbox, acknowledges the sender (one check mark, sent), and looks up the recipient's devices in the session registry.

Online recipient. The router forwards the message to the chat server holding the recipient's connection, which pushes it to the device. The device decrypts, stores it locally, and sends a delivery ack. The server deletes it from the inbox and relays a delivered receipt to the sender (two check marks).

Offline recipient. If no session exists, the message stays in the inbox and the push service sends a silent notification. When the device reconnects it drains its inbox in order, acknowledging each batch, and delivered receipts flow back to senders.

Read receipt. When the user opens the chat, the device sends a read receipt for the latest message ID. It is routed exactly like a small message, and the sender's device marks every earlier message as read.

Group message. The sender encrypts once with its sender key for the group and sends one copy. The router reads the membership list and writes a copy into each member device's inbox, then forwards to online members.

Holds persistent connections, authenticates devices, and moves encrypted payloads in and out.

  • Erlang or Elixir on BEAM, which handles millions of lightweight processes per host, or Go with an event loop.
  • A compact binary protocol over TLS on a long-lived TCP connection, with heartbeats every few minutes.

Scalability

Each server holds about 1 million connections, mostly idle. On connect a device registers device_id -> server_id in the session registry. Servers are stateless beyond their live sockets, so a crashed server only forces its clients to reconnect elsewhere and drain their inboxes.

Tradeoffs

Persistent connections are needed for sub-second delivery and cheap receipts; polling would waste battery and data. Mobile operating systems kill background sockets, so the design relies on push notifications to wake the app rather than trying to keep connections open forever.

Routes each message to its recipient devices and stores it until each device acknowledges it.

  • A wide-column store such as Cassandra or HBase for inboxes, tuned for write then delete.
  • Redis for the session registry with entries that expire if heartbeats stop.

Diagram

Loading diagram...

Data Model

  • inbox: device_id (partition key), server_seq (clustering key), message_id, sender_id, chat_id, ciphertext, created_at, with a 30 day TTL.
  • sessions: device_id -> { server_id, connected_at }.

Scalability

Partitioning inboxes by device spreads load evenly and makes draining an inbox a single ordered range read. Most messages are deleted within seconds, so the table stays small relative to the write rate; compaction settings favor fast tombstone cleanup.

Tradeoffs

Writing to the inbox before forwarding costs one write even for online recipients, but it is what makes delivery at least once: if the forward fails or the device crashes before acking, the message is still in the inbox. Devices deduplicate by message_id. The server keeps no history after delivery, which limits what a breach can expose and is consistent with end-to-end encryption.

Stores public identity keys and one-time prekeys so devices can start encrypted sessions with each other, even when the other side is offline.

API

  • PUT /keys uploads the device's identity key, signed prekey, and a batch of one-time prekeys.
  • GET /keys/:userId returns a prekey bundle for each of the user's devices and consumes one one-time prekey per device.

Data Model

  • identity_keys(user_id, device_id, identity_key, signed_prekey, signature)
  • one_time_prekeys(user_id, device_id, key_id, public_key)

Tradeoffs

The Signal protocol (X3DH handshake plus the double ratchet) gives forward secrecy, so stealing a device's current keys does not expose past messages. The server is trusted to hand out the right keys, which is why apps show safety numbers that users can compare in person. For groups, sender keys trade a little forward secrecy for sending one ciphertext instead of 255, which is why groups are capped at a few hundred members.

Uploads and serves encrypted attachments outside the message path.

  • Object storage such as S3 with a CDN in front.

API

  • POST /media returns a presigned upload URL and a media ID.
  • GET /media/:id redirects to a CDN URL.

Tradeoffs

The device encrypts the file with a random key, uploads the ciphertext, and sends the key, hash, and media ID inside the encrypted message. The server stores only opaque blobs. Sending media inline through chat servers would tie up connections and make retries expensive.

Tracks online status and last seen for contacts who are allowed to see it.

  • In-memory key-value store (Redis) with TTL-based expiry.

Scalability

Presence is only pushed to contacts who currently have the chat open, not to every contact, which keeps update volume proportional to active viewing rather than contact list size. Last seen is updated at most once a minute per user.

Tradeoffs

Presence is best effort and eventually consistent. Accuracy within a few seconds is enough, so it lives in a separate cache instead of the durable message path.

Related questions