Expert Case Studies

Designing WhatsApp: Real-Time Messaging, Presence, and End-to-End Encryption

Analyze how to design a chat system like WhatsApp. Cover persistent WebSocket connections, offline message delivery, last-seen status, group fan-out, and the Signal Protocol.

August 14, 2026
case studywhatsappmessagingwebsocketsend-to-end encryptionsignal protocolscale

Why Study WhatsApp?

WhatsApp handles over 2 billion users exchanging more than 100 billion messages a day, and famously ran this scale on a strikingly small engineering team (fewer than 50 engineers at the time of the Facebook acquisition). Unlike a feed system where staleness is tolerable, a chat system has to get delivery ordering, offline handling, and encryption right - a dropped or duplicated message is a product failure, not a rounding error. Studying WhatsApp reveals how to build stateful, connection-oriented systems at scale, which is a very different problem from the stateless request/response systems most web services are built around.

Approach: Chat system interviews reward depth over breadth. Interviewers will push hardest on three things: how a message reaches a device that's offline, how you guarantee messages aren't lost or duplicated, and how group messaging avoids the "celebrity fan-out" trap. Anchor your design around those three questions rather than trying to cover every feature.


Requirements Analysis

Functional Requirements

  1. 1-on-1 Messaging: Send and receive text, media, and voice messages between two users
  2. Group Messaging: Send messages to a bounded group (up to ~1024 members)
  3. Online/Last-Seen Status: Show whether a contact is currently online or when they were last seen
  4. Delivery Receipts: Track message states - sent, delivered, read (single/double/blue ticks)
  5. Offline Delivery: Messages sent to an offline user must be queued and delivered on reconnect
  6. End-to-End Encryption: Only the sender and recipient(s) can read message content - not even the server

Non-Functional Requirements

RequirementTarget
Scale2B+ users, 100B+ messages/day (~1.2M messages/sec average, much higher at peak)
LatencyMessage delivery to an online recipient in < 200ms
DurabilityMessages for offline users must never be lost, only delivered once confirmed
Delivery GuaranteeAt-least-once delivery with client-side deduplication (effectively exactly-once from the user's perspective)
OrderingMessages within a single conversation arrive in sent order
Availability99.99% uptime - a chat app going down is highly visible
Connection ScaleHundreds of millions of concurrent persistent connections

High-Level Architecture


Persistent Connection Architecture

Why WebSockets, Not Polling

HTTP request/response is a poor fit for chat: polling every few seconds wastes bandwidth and adds latency, and long-polling ties up server resources without solving bidirectional push cleanly. WhatsApp keeps a long-lived TCP connection (historically XMPP-based, now a custom binary protocol, conceptually equivalent to a WebSocket) open between each client and one gateway server for as long as the app is in the foreground.

The key insight is that gateway servers are stateful - each one holds an in-memory map of user_id -> socket. This breaks the usual "stateless app server" pattern used elsewhere in web systems, so gateways need their own routing layer in front of them.

Finding the Right Gateway for a User

When Message Service needs to deliver to a recipient, it doesn't know which of thousands of gateway servers holds that user's socket. It looks up a presence/routing table in Redis (user_id -> gateway_id), populated when the client connects and refreshed on every heartbeat, then either:

  1. Delivers via that gateway if the entry exists and the gateway confirms the socket is still open (handles the race where a gateway crashed without deregistering).
  2. Falls back to the offline mailbox queue if no entry exists, or the gateway reports the socket is gone.
ApproachDescriptionTradeoff
Sticky client reconnectClient always reconnects to the same gateway via DNS/anycastSimple, but creates hot spots and complicates deploys
Redis presence registryAny gateway can look up any user's current gatewayExtra hop, but decouples gateway scaling from client identity
Consistent hashing to gatewayuser_id hashed to a fixed gateway poolNo lookup needed, but rebalancing on scale-up is disruptive

WhatsApp-style systems use the registry approach: it's a small amount of extra latency (a Redis lookup, single-digit milliseconds) in exchange for gateways being freely interchangeable, which matters enormously for rolling deploys and autoscaling a fleet handling hundreds of millions of sockets.

💡

Why not just broadcast to all gateways? With hundreds of servers, broadcasting every message to every gateway to find the recipient would multiply write traffic by the fleet size. A directed lookup keeps the fan-out to exactly one gateway per online recipient.


Message Delivery Semantics

The Three Ticks

WhatsApp's delivery receipts map to three states, each written by a different party:

At-Least-Once Delivery + Client-Side Dedup

Network blips mean an ACK can be lost even though the message was delivered - the server can't always distinguish "recipient never got it" from "recipient got it but the ACK didn't make it back." Rather than risk under-delivery, the system resends on ambiguity, giving at-least-once delivery. To make this safe:

  • Every message carries a client-generated unique message_id (not server-assigned), so retries are idempotent.
  • The recipient's client keeps a small local set of recently-seen message_ids and silently drops duplicates before rendering.
  • The server only advances a message from "sent" to "delivered" once it gets an explicit ACK, and retries delivery on a backoff if no ACK arrives within a timeout while the recipient is online.

This combination - server-side at-least-once plus client-side dedup - produces exactly-once behavior from the user's point of view without needing distributed transactions.

Interview tip: If asked "how do you guarantee exactly-once delivery," the honest answer is that you don't do it at the network layer - you do at-least-once delivery with idempotent client-side dedup keyed on a client-generated message ID. This is the same pattern used by payment systems for idempotent retries.


Offline Message Queueing

Per-User Mailbox

Every user has a durable mailbox - conceptually a queue keyed by user_id, storing messages addressed to them that haven't yet been delivered. When the presence lookup shows a recipient is offline, the Message Service writes to that mailbox instead of pushing over a socket.

Messages stay in the mailbox until acknowledged as delivered, then get pruned (WhatsApp historically deleted messages from its servers once delivered, syncing only undelivered content - this is a direct consequence of E2E encryption meaning the server has no business retaining plaintext-adjacent content longer than necessary).

Design ChoiceRationale
Queue per user, not per conversationA user can have thousands of conversations; one queue simplifies "give me everything waiting for me" on reconnect
FIFO drain on reconnect, ordered by original send timePreserves conversation ordering per sender
Cap mailbox size / TTL (e.g. 30 days)Bounds storage for accounts that never come back online
Push in original send order per-senderCross-sender interleaving is less critical than per-sender order

Last-Seen and Online Status

Presence Heartbeats

Online status is tracked with lightweight heartbeats rather than being inferred solely from socket state, since a socket can silently die (mobile networks drop connections without a clean TCP close). The gateway expects a heartbeat/ping from the client every N seconds (commonly 30-60s); missing a couple of heartbeats marks the user offline and updates last_seen to the last confirmed activity timestamp.

Privacy Considerations

Presence is one of the more privacy-sensitive features in a chat app:

  • Opt-out asymmetry: if a user hides their last-seen, they typically also lose the ability to see others' last-seen - a reciprocity rule to discourage one-sided surveillance.
  • Contact-list scoping: presence updates are only pushed to users who share a conversation/contact relationship, not broadcast globally - this bounds fan-out to a person's actual contacts rather than all 2B users.
  • Fuzzing granularity: last-seen is often bucketed ("today," "yesterday," rather than exact timestamps) to reduce precise activity tracking.
💡

Why heartbeats instead of trusting TCP state? Mobile networks (especially cellular) frequently drop connections without either side observing a clean close (a "half-open" connection). Relying on TCP FIN/RST to detect disconnects would leave users appearing online long after they've actually dropped off.


Group Messaging Fan-Out

Why Groups Don't Scale Like Twitter

A key distinction from feed-fan-out problems (like Instagram's celebrity account problem) is that WhatsApp groups are bounded - capped at around 1024 members - whereas a Twitter-style follower fan-out is unbounded (millions of followers). This bound is a deliberate design constraint, not an accident: it keeps the fan-out cost of every group message small and predictable.

ApproachDescriptionTradeoff
Server fan-out (used here)Server iterates the bounded member list and delivers/queues per memberBounded cost since group size is capped; simple to reason about
Client-side fan-outSender's client encrypts and sends N separate messages, one per memberAvoids server complexity but multiplies sender's upload bandwidth and battery cost
Unbounded fan-out (not used)Treat groups like follower lists with no capWould require celebrity-style push/pull hybrid logic - WhatsApp avoids this entirely by capping group size

Capping group size is what allows WhatsApp to skip the entire push-vs-pull hybrid model that feed systems need: since the maximum fan-out per message is small and fixed, straightforward per-member delivery through the same offline-mailbox mechanism used for 1:1 chat is sufficient at any group size the product allows.


End-to-End Encryption: The Signal Protocol

End-to-end encryption is the requirement that shapes almost everything else about a chat system's server-side capabilities: since only the two (or group) endpoints can decrypt content, the server cannot do content-based search, spam filtering, or ranking on message bodies - it can only route encrypted blobs. WhatsApp uses the Signal Protocol, at a conceptual level:

Key Agreement (X3DH)

Before two users exchange a single message, their clients perform an Extended Triple Diffie-Hellman (X3DH) key agreement. Each user publishes a bundle of public keys to the server ahead of time (an identity key, a signed prekey, and a batch of one-time prekeys). When Alice wants to message Bob for the first time - even while Bob is offline - she fetches Bob's key bundle from the server and combines several Diffie-Hellman exchanges to derive a shared secret that only Alice and Bob could compute. Critically, this works without Bob being online, because his prekeys were pre-published.

Double Ratchet (Ongoing Messages)

Once the initial shared secret is established, ongoing messages use the Double Ratchet algorithm, which combines two mechanisms:

  1. A symmetric-key ratchet that derives a fresh message key for every single message from a continuously-advancing chain, so compromising one message's key doesn't expose others.
  2. A Diffie-Hellman ratchet that periodically mixes in a new DH exchange whenever the conversation direction changes, providing "self-healing" - even if an attacker briefly compromises a device's key state, future messages become secure again as new DH values are introduced.

This gives forward secrecy (past messages stay safe if a key is later compromised) and post-compromise security (future messages recover safety after a compromise). The exact key-derivation math is out of scope here - the important takeaway for system design purposes is what the protocol guarantees and what it costs the server.

What E2E encryption takes off the table server-side: No server-side full-text message search (search must happen locally on-device against locally-decrypted content). No server-side spam/abuse content filtering on message bodies (WhatsApp instead relies on metadata signals - message rate, block/report rates, group-join patterns). No server-side message previews for notifications on some platforms without special handling. This is a genuine product tradeoff, not just a crypto detail, and interviewers care whether you recognize the consequence, not just the algorithm name.


Database Schema

Conversations

sql
CREATE TABLE conversations (
    id BIGINT PRIMARY KEY,
    type VARCHAR(10) NOT NULL, -- 'direct' or 'group'
    created_at TIMESTAMP DEFAULT NOW()
);
 
CREATE INDEX idx_conversation_type ON conversations(type);

Group Members

sql
CREATE TABLE group_members (
    conversation_id BIGINT NOT NULL REFERENCES conversations(id),
    user_id BIGINT NOT NULL REFERENCES users(id),
    role VARCHAR(10) DEFAULT 'member', -- 'admin' or 'member'
    joined_at TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (conversation_id, user_id)
);
 
CREATE INDEX idx_group_members_user ON group_members(user_id);
-- Enforced at application layer: max ~1024 rows per conversation_id

Messages

sql
CREATE TABLE messages (
    id BIGINT PRIMARY KEY,           -- client-generated, used for dedup
    conversation_id BIGINT NOT NULL REFERENCES conversations(id),
    sender_id BIGINT NOT NULL REFERENCES users(id),
    ciphertext BLOB NOT NULL,        -- server never sees plaintext
    sent_at TIMESTAMP DEFAULT NOW(),
    -- transient: purged once delivered to all recipients (E2E design goal)
    status VARCHAR(10) DEFAULT 'sent'
);
 
CREATE INDEX idx_messages_conversation ON messages(conversation_id, sent_at DESC);
CREATE INDEX idx_messages_sender ON messages(sender_id);

Delivery Receipts

sql
CREATE TABLE delivery_receipts (
    message_id BIGINT NOT NULL REFERENCES messages(id),
    recipient_id BIGINT NOT NULL REFERENCES users(id),
    status VARCHAR(10) NOT NULL, -- 'sent', 'delivered', 'read'
    updated_at TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (message_id, recipient_id)
);
 
CREATE INDEX idx_receipts_recipient ON delivery_receipts(recipient_id, status);
💡

Why is messages transient? Because content is end-to-end encrypted and the client is the durable source of truth for message history (stored locally, optionally backed up encrypted to cloud storage), the server only needs to retain a message long enough to guarantee delivery - not forever. This is a deliberate departure from systems like Instagram's posts table, which is the permanent system of record.


Operational Strategy: Connection and Queue Scaling

Gateway Capacity Planning

Each gateway server holds one socket per connected user, so capacity is bounded by file descriptors and per-connection memory rather than CPU. A single modern server can hold on the order of 1-2 million idle WebSocket connections with careful tuning (epoll-based event loops, minimal per-connection memory footprint). At 2B users with a fraction concurrently online, this still requires thousands of gateway servers, each registering its held connections into the shared Redis presence registry.

Mailbox and Queue Backpressure

ConcernMitigation
Gateway crash mid-sessionClient detects dropped socket via missed heartbeat ACK, reconnects to a new gateway, re-registers presence
Presence registry as single point of failureRedis Cluster with replication; a stale/missing entry just falls back to the offline mailbox path (degrades gracefully, doesn't lose messages)
Mailbox growth for long-inactive usersTTL-based expiry (e.g. 30 days) with a warning that undelivered messages older than the TTL are dropped, matching WhatsApp's real behavior
Thundering herd on reconnect (e.g. after an outage)Staggered/rate-limited reconnect backoff on the client, mailbox drain paginated rather than pushed all at once

Scaling Challenges & Solutions

ChallengeSolution
Millions of concurrent stateful connectionsDedicated gateway tier tuned for connection count, not compute; horizontally scaled
Finding which gateway holds a user's socketRedis-backed presence registry (user_id -> gateway_id) refreshed on heartbeat
Message loss on recipient offlineDurable per-user mailbox queue, drained on reconnect
Duplicate delivery from retriesClient-generated message IDs + client-side dedup
Group fan-out costBounded group size (~1024) keeps per-message fan-out small and predictable
Content search/spam filtering under E2EPush these features client-side or rely on metadata signals, not message content
False "offline" from dropped mobile connectionsHeartbeat-based presence with grace period, not raw TCP state

Key Takeaways

  1. Stateful gateways need their own routing layer: unlike stateless API servers, WebSocket gateways hold state (the socket), so you need a presence registry to route to the right one
  2. At-least-once + client dedup beats trying for exactly-once server-side: idempotent client-generated IDs make retries safe without distributed transactions
  3. Offline delivery is a queue problem, not a database problem: a per-user mailbox that drains on reconnect is simpler and more scalable than polling
  4. Bounding group size sidesteps the celebrity fan-out problem entirely: a hard cap on membership avoids the push/pull hybrid complexity feed systems need
  5. End-to-end encryption is a product tradeoff, not just a crypto checkbox: it forecloses server-side content features, which should shape the rest of your design

Interview tip: When a chat system design gets abstract, ground it by walking through one concrete trace: "Alice sends a message while Bob is offline." Narrate presence lookup, mailbox write, ACK to Alice, then Bob's reconnect and drain. This single trace touches nearly every core component and demonstrates you understand the mechanics, not just the component names.


Follow-Up Questions to Consider

  1. How would you support multi-device sync (same account logged in on phone + web + desktop simultaneously)?
  2. How would you design encrypted media (photos/videos) attachments, given E2E means the server can't preview or transcode them?
  3. How would you handle a group admin removing a member mid-conversation - what happens to encryption keys?
  4. How would you build spam/abuse detection without reading message content?
  5. How would you design "message backup to cloud" while preserving end-to-end encryption guarantees?
💡

Real WhatsApp trivia: WhatsApp's core messaging infrastructure was famously run by a tiny engineering team - around 50 engineers were supporting 900 million users at the time of the 2014 Facebook acquisition, a ratio often cited as one of the most efficient in tech history. The backend was originally built on Erlang, chosen specifically for its strength in handling massive numbers of concurrent lightweight connections - the same problem this article's gateway tier is solving.