Designing WhatsApp
The connection tier, the send path and its guarantees, the offline queue, groups, end-to-end encryption and multi-device, receipts, push notifications and media.
On this page
Messaging inverts the assumption every other system in this series makes. There is no request waiting for a response: the server must push to a device that may be asleep, on a train, or offline for three days, and it must never lose the message or show it twice. On top of that, the server is not allowed to read anything it is carrying. The interesting parts of this design are the connection tier, the ordering guarantees, and what encryption takes away.
The map
Read this first when short on time. Every branch is a section below.
What it must do, and at what size
It must deliver a message from one device to one or more others exactly once, in a consistent order within a conversation, whether the recipient is online now or in three days; report delivery and read status; support groups; and keep content unreadable by the servers carrying it. It must not, given that last requirement, offer server-side search, server-side spam filtering on content, or web access without a paired device holding keys.
Users 2 billion, ~500 million devices connected at any moment
Messages 100 billion/day -> ~1.16 million/s average, ~3 million/s peak
Message size ~100 bytes of ciphertext for text, media stored separately
Receipts 2-3 status events per message -> the control plane is
roughly as busy as the data plane
Connection ~10 KB of server memory per idle socket
500M sockets x 10 KB = 5 TB of memory across the gateway tier
at 1M sockets per gateway -> ~500 gateway machines
Fan-out a 256-member group turns 1 send into 256 deliveries
and 256 delivery receiptsThe shape here is unusual and worth naming. The messages are tiny, so bandwidth is not the constraint. Connections are the constraint: five hundred million mostly-idle sockets is the dominant cost, and it is a memory and file-descriptor problem rather than a throughput one. The second surprise is that receipts double or triple the message rate — every message produces a delivered event and often a read event, each of which is itself a message that must be routed and stored.
flowchart TD D1["sender device"] --> G1["gateway, holds the socket"] G1 --> CS["chat service"] CS --> ST[["message store, per conversation"]] CS --> RG[["session registry: device to gateway"]] CS --> BR[["delivery bus, partitioned by conversation"]] BR --> G2["recipient's gateway"] G2 --> D2["recipient device"] CS --> PN["push service, for offline devices"] PN --> D2
The connection tier
Every online device holds one long-lived connection so the server can push. That tier is deliberately the dumbest part of the system: it terminates TLS, holds the socket, and forwards frames. It keeps no business logic and no durable state, which is what lets it be scaled and restarted independently of everything else.
The session registry maps a device to the gateway currently holding its socket. It is a small record with a short TTL, refreshed by the gateway's heartbeat, so a gateway that dies has its entries expire without anyone running cleanup. Delivering to a user means looking up their devices, finding each one's gateway, and publishing to it.
Keepalives are a battery decision as much as a networking one. Every ping wakes the phone's radio, and a radio wake costs far more energy than the bytes suggest, so intervals are long — minutes, not seconds — and tuned per network, because carrier NAT tables drop idle mappings on their own schedule. The client's reconnect uses exponential backoff with jitter, which matters more here than almost anywhere: when a gateway restarts, a million devices try to reconnect at once, and without jitter they arrive as one wave.
The reconnect storm is the failure mode that turns a routine deploy into an outage. Drain gateways slowly, spread the reconnect over minutes with client-side jitter, and make sure the registry and the authentication path can absorb the burst — they are the two things every reconnecting device touches.
- WhatsApp famously ran on Erlang and demonstrated millions of connections per server, which is the origin of the separate socket-holding tier in most later designs.
- Slack and Discord both split the socket gateway from the service layer for exactly this reason: the two scale on different resources.
- MQTT brokers solve the same problem for devices, with keepalive intervals negotiated per client for the same battery reasons.
Sending one message
The send path is short and every step in it is load-bearing.
sequenceDiagram autonumber participant S as Sender participant G as Gateway participant C as Chat service participant R as Recipient S->>G: send, client id 9f2, ciphertext G->>C: persist and assign a server id C-->>G: stored, id 41207 G-->>S: one tick, the server has it C->>R: deliver via the recipient's gateway R-->>C: delivered receipt C-->>S: two ticks R-->>C: read receipt when opened C-->>S: two blue ticks
Persist before delivering. The server acknowledges only after the message is durable, so a gateway crash between receipt and storage is a retry rather than a loss. This is the ordering that makes the whole system trustworthy, and reversing it for latency would be the worst trade available here.
The client id makes retries safe. The sender generates an id before sending; if the acknowledgement is lost and the client retries, the server recognises the id and returns the original result instead of storing a second copy. Without it, every flaky connection produces duplicate messages — the most visible possible bug.
Ordering is per conversation. The server assigns a monotonic id and the delivery bus is partitioned by conversation id, so every device in a conversation sees the same order. Across conversations there is no order and none is needed. Clients sort by server id rather than arrival, so a message that arrives late still lands in the right place.
Note what the three states actually mean: the first tick is "the server stored it", the second is "the recipient's device acknowledged receipt", and the third is "the application was opened on that message". They are three different events in three different places, which is why a product that tries to collapse them into one always ends up lying about something.
The store is a queue, not an archive
Here is a decision that looks like a storage detail and is really a product and privacy decision: how long does the server keep a message?
In an end-to-end encrypted messenger, the server's copy is ciphertext it cannot read, useful only for delivery. So the store behaves as a per-device outbound queue: hold the message until every one of the recipient's devices has acknowledged it, then delete it, with a hard expiry of some days for devices that never come back. Undelivered messages accumulate per device and are drained in order on reconnect, where the client asks for everything after the last id it holds.
| Queue model (WhatsApp) | Archive model (Slack) | |
|---|---|---|
| Server keeps | Until delivered, then deletes | Everything, forever, searchable |
| Storage cost | Proportional to undelivered backlog | Proportional to all history ever |
| New device | Sees nothing, or restores from a user backup | Sees full history immediately |
| Search | On device only | Server-side, across the workspace |
| Follows from | End-to-end encryption | Server-readable content |
The two models are not interchangeable options: the encryption choice decides the storage model, which decides the product. Once the server cannot read messages, server-side search and instant history on a new device stop being features that were skipped and become features that cannot exist.
Groups
A group message is one send and N deliveries. The server expands the member list and routes a copy to every device of every member, which is the same fan-out problem as the news feed with two differences that make it much easier.
First, groups are bounded. A few hundred to about a thousand members is a deliberate product limit, and it keeps the worst-case fan-out three orders of magnitude below a social feed's. Anything larger is a different product — a broadcast channel — with a different design, because it is a one-to-many publication rather than a conversation.
Second, a group is one conversation, so it is one partition, so ordering is free: every member sees the same sequence because every message passes through the same ordered stream. That is a strong guarantee cheaply obtained, and it is why group chat feels consistent in a way feeds never do.
flowchart TD M["one group message"] --> C["chat service, conversation partition"] C --> E["expand to 256 members"] E --> D["resolve each member's devices, about 400 sockets"] D --> S[["deliver per device, store for the offline ones"]] S --> R["256 delivery receipts come back"] R --> C
Receipts are the hidden cost of groups. A 256-member group generates 256 delivery receipts and up to 256 read receipts for every message, so the control traffic exceeds the message traffic by two orders of magnitude. Aggregate them — send the sender a count rather than an event per member, and batch them on a short timer.
End-to-end encryption, and what it takes away
The server carries ciphertext it cannot decrypt. That single property reshapes the rest of the design, so it is worth understanding the mechanism well enough to know what it costs.
Starting a session without both parties online. Each device publishes an identity key, a signed prekey and a batch of one-time prekeys to the server. A sender fetches one bundle, combines it with their own keys, and derives a shared secret unilaterally — so the first message can be sent to a device that is offline. The server stores public prekeys and never a secret; the worst it can do is hand out a bundle it has tampered with, which is what identity-key verification and key-change warnings exist to expose.
A new key per message. The session then advances a ratchet: every message derives a fresh key, and the chain moves forward with each exchange. Two properties follow, and they are the point of the whole construction. Forward secrecy: compromising a device today does not decrypt yesterday's messages, because those keys are gone. Recovery: a compromise is healed once a new exchange mixes in fresh key material. Out-of-order arrival is handled by keeping a small window of skipped message keys, which is why a message that arrives an hour late still decrypts.
Groups need a different trick. Encrypting a message separately for every member's every device would mean hundreds of encryptions per send. Instead each sender generates a sender key for the group, distributes it once to each member through the pairwise sessions, and thereafter encrypts each message once with that key. Sends become cheap; the cost is that the key must be rotated and redistributed whenever a member leaves, or a former member could still read new traffic.
- The Signal protocol is the implementation used by Signal, WhatsApp and Messenger's secret conversations, with the prekey and ratchet design described here.
- Multi-device support arrived late in these products precisely because it means each device is a separate cryptographic identity, so every message is encrypted once per device rather than once per person.
- Server-side spam and abuse handling in encrypted messengers works on metadata and reports rather than content, which is a direct and unavoidable consequence of the design.
- Prekey bundle
- Public keys a device publishes in advance so that others can start an encrypted session with it while it is offline.
- Forward secrecy
- Old messages stay unreadable even if a device's current keys are compromised, because the keys that encrypted them no longer exist.
- Sender key
- A per-sender group key distributed once through pairwise sessions, so group messages need one encryption rather than one per recipient.
Presence, typing and receipts
These three features look trivial next to encryption and are collectively a larger share of the traffic than messages.
Presence is a key with a short TTL refreshed by the device's heartbeat, so a dropped connection becomes offline without anyone sending a message. The expensive part is distribution: pushing every status change to every contact is a multiplicative fan-out of events nobody is looking at. The fix is to fan out only to watchers — people with that conversation currently open — and answer everyone else on demand when they open a chat.
Typing indicators are ephemeral, never stored, throttled to at most one event every few seconds, and expire on their own after a short timeout so a client that disappears mid-sentence does not leave a permanent "typing…" on someone's screen.
Receipts are the ones that cost real money, because each is a routed, stored, acknowledged event of its own. Batch them: a device that receives twenty messages while offline sends one receipt covering all of them, and read receipts for a conversation collapse into a single high-water mark — "read up to message 41207" — rather than one event per message. That change alone removes most of the control-plane traffic.
Push notifications
A device without a live connection is reached through the platform's push service, which is the only way to wake an application the operating system has suspended.
sequenceDiagram autonumber participant C as Chat service participant P as Push service participant D as Device C->>C: recipient has no live socket C->>P: wake device, no message content P->>D: notification D->>C: reconnect and fetch after last id C-->>D: the queued messages Note over D: the client decrypts and renders the alert
Two constraints shape this. The payload cannot carry the message, both because the server cannot read it and because platform services are third parties; so the notification is a wake-up, and the client fetches and decrypts before displaying anything. And platform delivery is best effort — throttled, batched by the operating system, and occasionally dropped — so it can never be the only delivery mechanism. The message is already durably stored; push only shortens the time until the device asks for it.
Media
Photos and video do not travel through the message path. The client encrypts the file with a fresh key, uploads the blob to object storage, and sends a message containing the pointer, the key and a small inline thumbnail.
Three consequences are worth noting. The blob store sees only ciphertext, so it can be a plain CDN-fronted object store with no special handling. Download is a normal edge-cached fetch, which is what makes media fast globally without any of it being readable in transit or at rest. And forwarding does not re-upload: the same blob is referenced by a new message with the same key, so a video forwarded a million times is stored once — which is the single largest saving in the media path, and also why forwarded-message limits exist as a policy control rather than a technical one.
What breaks first
- Connections per gateway. The dominant cost, and it is memory and file descriptors rather than throughput. Adding gateways is easy; the thing to watch is the reconnect burst when one is drained.
- Group fan-out. Members times devices, times receipts. Capping group size is the lever, and it is a product decision rather than an engineering one — which is why the cap exists in every messenger.
- The control plane. Receipts, presence and typing together exceed message traffic. Batching and high-water-mark receipts are what keep it bounded.
- Push service quotas. An external dependency with its own limits and its own throttling behaviour, and the one part of the path that cannot be scaled by spending money on your own hardware.
The judgement that runs through all of it: delivery beats latency. Persisting before acknowledging, retrying until receipted, and keeping undelivered messages for days are all trades of speed for certainty, and they are the right way round for this product in a way they would not be for a feed.
Recap
- The constraint is connections, not bandwidth: five hundred million mostly-idle sockets at around 10 KB each is a memory and descriptor problem spread over hundreds of gateways.
- Gateways hold sockets and nothing else. A session registry with a short TTL maps device to gateway, so a dead gateway's entries expire on their own.
- Keepalives are a battery decision: long intervals, tuned per network, with jittered reconnect backoff to survive the storm when a gateway drains.
- Persist before delivering, so a crash is a retry rather than a loss; the first acknowledgement means stored, not read.
- A client-generated id makes retries idempotent, and a server-assigned monotonic id fixes the order within a conversation.
- Ordering is per conversation only, achieved by partitioning the delivery bus on conversation id — which is also why group chat feels consistent.
- With end-to-end encryption the server's copy is undeliverable ciphertext, so the store is a per-device queue that deletes after delivery rather than an archive.
- That choice removes server-side search and instant history on a new device — not features skipped, but features that cannot exist.
- Groups are bounded at roughly a thousand members, which keeps fan-out three orders of magnitude below a social feed's.
- Prekey bundles let a session start with an offline device; a ratchet gives a fresh key per message, forward secrecy and recovery from compromise.
- Group encryption uses a sender key distributed once per member, so a send is one encryption — and the key must rotate when anyone leaves.
- Multi-device means each device is its own cryptographic identity, so a message is encrypted once per device rather than once per person.
- Receipts, presence and typing together outweigh message traffic; batch receipts and collapse read receipts into a high-water mark.
- Push is a doorbell, not a delivery mechanism: best effort, carries no content, and only shortens the time until the device fetches what is already stored.
- Media is client-encrypted, uploaded once to object storage and referenced by pointer, so forwarding costs nothing and the CDN never sees plaintext.
Questions
Try answering each one out loud before opening it. Lead with the one-line answer, then the reasoning, then one detail from a real system.
What is the dominant cost in a messaging system, and why is it surprising?
Idle connections. Five hundred million devices each holding a socket at roughly ten kilobytes of server memory is about five terabytes across the gateway tier, while the messages themselves are a hundred bytes each and barely register as bandwidth.
- It is a memory and file-descriptor problem, not a throughput one, which is why the gateway tier does nothing but hold sockets.
- The second surprise is that receipts and presence traffic exceed message traffic.
- Scaling means more gateways with a smaller share of connections each.
WhatsApp's early demonstrations of millions of connections per machine are the reason this tier is designed as a specialised thing rather than as ordinary web servers.
How does a message reach a specific device?
The chat service looks the device up in a session registry that maps it to the gateway currently holding its socket, then publishes to that gateway, which writes the frame down the connection.
- The registry entry has a short TTL refreshed by heartbeat, so a dead gateway's entries expire without a cleanup job.
- If there is no live socket, the message stays in the device's queue and a push notification wakes it.
- Multi-device means resolving a user to several devices and delivering to each.
Keeping the registry separate from the gateways is what lets either be restarted without the other losing track.
Why persist the message before acknowledging the sender?
So that a crash between acceptance and storage is a retry rather than a lost message — the acknowledgement is a promise of durability, and reversing the order to save latency would break the one guarantee the product is built on.
- The first tick means stored on the server, not delivered and not read.
- The three status states correspond to three genuinely different events in three different places.
- Delivery beats latency throughout this design, which is the opposite of a feed's priorities.
Collapsing the states into one is how products end up displaying something that is not true.
How are duplicate messages prevented when a client retries?
The sender generates a client id before sending, and the server treats it as an idempotency key: a retry with the same id returns the original result instead of storing a second copy.
- Without it, every lost acknowledgement on a flaky mobile connection produces a visible duplicate.
- The server additionally assigns its own monotonic id, which is what fixes the ordering.
- Clients sort by server id rather than by arrival time, so a late message still lands in the right place.
This is the same idempotency-key pattern as a payment API, applied to the highest-volume write in the system.
What ordering guarantee does a chat system need, and how is it obtained?
Total order within a conversation and none across conversations, obtained by partitioning the delivery stream on conversation id so every message for a chat passes through one ordered partition.
- Ordering across unrelated conversations has no meaning, which is exactly what makes this scale.
- A group is one conversation, so all members see the same sequence for free.
- Server-assigned monotonic ids let clients sort correctly regardless of arrival order.
It is the partition-key rule from the messaging article applied to a product guarantee users can see.
Why does an encrypted messenger delete messages from the server after delivery?
Because the server's copy is ciphertext it cannot read, so it has no value beyond delivery — the store is a per-device queue held until every device acknowledges, with a hard expiry after some days.
- Storage cost then tracks the undelivered backlog rather than all history ever sent.
- A new device sees no history unless the user restores their own backup.
- The alternative archive model, as in Slack, only works because the server can read the content.
The encryption decision determines the storage model, which determines what the product can offer — they are not independent choices.
Why are group sizes capped, and what does a group message actually cost?
Because fan-out is members times devices and receipts travel back along the same path, so a 256-member group turns one send into several hundred deliveries and several hundred receipts; the cap keeps that bounded.
- Anything much larger is a broadcast product rather than a conversation and needs a different design.
- Receipts are the dominant cost, which is why they are aggregated into counts rather than sent per member.
- Because a group is one conversation it is one partition, so ordering comes free.
The cap is a product decision doing engineering work, which is why every messenger has one.
How can a message be encrypted for a recipient who is offline?
Each device publishes an identity key, a signed prekey and a batch of one-time prekeys in advance; the sender fetches a bundle and derives a shared secret unilaterally, so the first message needs no interaction with the recipient.
- The server stores only public keys and never a secret.
- Its remaining power is to hand out a tampered bundle, which identity verification and key-change warnings are designed to expose.
- One-time prekeys are consumed as they are used, so the server must be replenished by the client.
This asynchronous key agreement is what makes end-to-end encryption workable for mobile messaging at all.
What does the message ratchet give you?
A fresh key per message, which yields forward secrecy — a compromise today cannot decrypt yesterday's messages because those keys no longer exist — and recovery, since a later exchange mixes in new material and heals the compromise.
- A small window of skipped message keys is retained so out-of-order arrivals still decrypt.
- The cost is per-device session state that must be kept in sync and can break if a device restores an old backup.
- It is why a reinstalled application produces a key-change warning rather than silently continuing.
Those warnings are a security feature that exists precisely because the server is the one handing out key material.
How do you encrypt a group message without encrypting it hundreds of times?
Each sender generates a sender key for the group and distributes it once to every member over the pairwise encrypted sessions; after that, each message is encrypted once with that key and delivered to everyone.
- Without it, a send would require one encryption per member device — hundreds of operations for one message.
- The key must be rotated and redistributed when a member leaves, or they could still read new traffic.
- Membership changes are therefore more expensive than messages, which is the right way round.
It is a neat example of paying a one-off cost per participant to make the repeated operation cheap.
What does multi-device support cost in an end-to-end encrypted system?
Each device becomes its own cryptographic identity with its own session, so every message is encrypted once per device rather than once per person, and delivery, receipts and key distribution all multiply by the device count.
- History does not follow a new device automatically — it has to be transferred from an existing one.
- Removing a device must revoke its ability to decrypt future messages, which means rekeying.
- This is why multi-device arrived years after the encryption itself in these products.
The design choice that keeps it tractable is refusing to make any one device the master.
Why is presence expensive, and how is it made cheap?
Because pushing every status change to every contact is a fan-out proportional to the social graph, producing enormous volumes of events nobody is looking at; the fix is to notify only people with that conversation open and answer everyone else on demand.
- Status itself is a key with a short TTL refreshed by heartbeat, so a dropped connection expires to offline with no explicit message.
- Coarse states and a few seconds of staleness are perfectly acceptable, which is what makes the cheap design viable.
- Typing indicators are throttled and expire on their own for the same reasons.
It is the clearest case in the article of a feature whose cost comes from the graph rather than from the traffic.
How would you stop read receipts overwhelming the system?
Collapse them into a high-water mark — "read up to message 41207" — instead of one event per message, and batch delivery receipts for a backlog into a single acknowledgement.
- Each receipt is otherwise a routed, stored, acknowledged event as expensive as a message.
- In a group, send the sender an aggregate count rather than an event per member.
- A device coming back from hours offline would otherwise emit hundreds of individual receipts at once.
Control-plane traffic exceeding data-plane traffic is the normal state of a messenger, and this is the main lever against it.
What role does the push notification service play?
It is a doorbell: it wakes an application the operating system has suspended so that the device reconnects and fetches what is already durably stored — it never carries the message.
- The payload could not carry content anyway, since the server holds only ciphertext and the push service is a third party.
- Platform delivery is best effort and throttled, so it can never be the only delivery mechanism.
- The client decrypts locally before displaying the alert text.
It is also the one dependency in the path that cannot be scaled by buying more of your own hardware.
How is media handled, and why is forwarding cheap?
The client encrypts the file with a fresh key, uploads the blob to object storage, and sends a message carrying the pointer, the key and a small thumbnail; forwarding reuses the same blob with a new message, so nothing is re-uploaded.
- The blob store and CDN see only ciphertext, so they need no special handling.
- Download is an ordinary edge-cached fetch, which is what makes media fast worldwide.
- A video forwarded a million times is stored once, which is the largest saving in the media path.
It also explains why forward limits are a policy control: the technical cost of forwarding is close to zero.
A gateway is drained for a deploy and a million devices reconnect. What happens?
They arrive as a wave against the authentication path and the session registry, which are the two things every reconnecting device touches, and without jittered backoff that wave can be larger than normal peak traffic.
- Drain gateways slowly and spread reconnects over minutes rather than dropping all sockets at once.
- Client-side exponential backoff with jitter is the main defence.
- The registry must absorb a burst of writes as every device re-registers its new gateway.
It is the most common way a routine deployment becomes an incident in a system built on long-lived connections.