WhatsApp is often used as the textbook example of a chat system, but most textbook designs miss what makes it unusual: the servers move ciphertext they cannot read. Once message content is end-to-end encrypted, many things a normal backend does become impossible or move to the client. The server cannot search messages, deduplicate media by content, fan out group messages by decrypting once, or rebuild a lost device's history.

This article explains the system from first principles. It covers the connection layer that keeps a socket open to every active phone, the routing registry and offline queue, the key directory that lets strangers start encrypted sessions, the way one message becomes many ciphertexts for multi-device and groups, and the media path. A worked example follows one message from send to blue ticks. Specific facts come from WhatsApp's security whitepaper, its privacy policy and engineering posts. Details of the internal server stack come only from public talks, so they are labelled as historical.

Advertisement

The requirements that shape everything

Five product constraints decide the architecture. Recipients may be offline for days, so the system must store and forward. Online delivery should take well under a second, which means a persistent connection per active device. Content is end-to-end encrypted, so the server routes opaque blobs. One account has several devices, each with its own keys. And a group of 1,024 members, the limit WhatsApp announced in 2022, must not cost 1,024 times the sender's upload.

Those constraints imply four server-side jobs: hold connections, know where every device is connected, queue for devices that are not connected, and publish public keys. Everything else, including encryption, decryption, group membership checks on content and history, happens on clients.

WhatsApp message path: the server routes ciphertext it cannot readAlice phoneSignal sessionsAlice laptopcompanion deviceChat serversNoise-encrypted TCPper-device ctSession registryuser, device -> serverOffline queueup to 30 daysKey directoryidentity + prekeysBob phoneonline: push nowBob tabletoffline: queueddeliveron reconnectMedia blob storeencrypted filesPush serviceAPNs / FCM wake-upupload ciphertextwakeapp reconnectsPayload keys never leave devices. Servers see routing metadata, sizes and timing, not content.One logical message to a user with N devices becomes N ciphertexts, one per Signal session.
Figure 1. The sender encrypts once per recipient device. Chat servers look up each device in the session registry, push to connected devices, queue for disconnected ones and send a content-free push notification to wake them. Media goes to a blob store as ciphertext.

The connection layer

Each active app keeps one long-lived TCP connection to a chat server. That makes the chat tier stateful: a given device is reachable only through the server that holds its socket, so the system needs a registry mapping user and device to server.

The transport is encrypted separately from message content. WhatsApp's whitepaper describes Noise Pipes with Curve25519, AES-GCM and SHA-256 between client and server. Transport encryption hides routing metadata from network observers; end-to-end encryption hides content from WhatsApp itself.

Public engineering talks from around 2012 to 2014 described the chat tier as Erlang on FreeBSD, derived from the ejabberd XMPP server with a compact binary protocol replacing XML. A 2012 post reported about two million connections on one server. Treat that as history, not a description of today's infrastructure. The lesson still holds: most sockets are idle, so a cheap process per connection and a small memory footprint per socket matter more than CPU.

Mobile operating systems kill background sockets. A device without a socket gets a content-free push through APNs or FCM, reconnects and drains its queue.

Advertisement

Keys, prekeys and why the server publishes them

The recipient may be offline when the first message is sent, so Signal, which WhatsApp adopted in 2016, uses prekeys. At registration each device generates a long-term Curve25519 identity key pair, a signed prekey that rotates periodically, and a batch of one-time prekeys. It uploads only the public halves. The server's key directory hands a sender one bundle per recipient device: identity key, signed prekey and one unused one-time prekey, which the server then deletes so it is never reused.

The sender combines several Diffie-Hellman outputs from that bundle with its own keys to derive a shared secret without the recipient being online. From then on the Double Ratchet derives a fresh key per message, giving forward secrecy and recovery after compromise. The ratchet mechanics are covered in the Signal Protocol and Double Ratchet; here what matters is the architectural consequence: a session is between two devices, not two users. That one fact drives the multi-device and group designs below.

The key directory is the most trusted part of the server. If it handed out a key it controlled, it could read new messages. In 2023 WhatsApp also announced key transparency based on an auditable key directory, so clients can check that the keys they are served match a publicly verifiable log.

Worked example: one message from send to blue ticks

Alice sends Bob a text. Bob has an online phone and a sleeping tablet; Alice has a phone and a laptop.

  1. Alice's phone fetches Bob's device list and, for any device it has no session with, a prekey bundle. It also includes Alice's own laptop, so her other device shows the sent message.
  2. It encrypts the plaintext three times, once per Signal session: Bob's phone, Bob's tablet, Alice's laptop.
  3. It sends one envelope over its Noise connection: a message id, the recipient, and a list of device-addressed ciphertexts.
  4. The chat server acknowledges receipt. Alice sees one grey tick: the server has the message.
  5. For each device it looks up the registry. Bob's phone is connected, so the ciphertext goes straight down that socket. Bob's tablet is not, so its ciphertext is written to the offline queue and a content-free push is sent.
  6. Bob's phone decrypts, stores the message locally and sends a delivery receipt. Alice sees two grey ticks.
  7. Bob opens the chat; his phone sends a read receipt and Alice sees blue ticks, if both have read receipts enabled.
  8. Hours later the tablet reconnects, drains the queue, decrypts its own copy and acknowledges, and the server deletes the queued entry.

The routing code on the server is short, because the server has nothing to interpret:

def route(server, msg):
    """Called on the chat server holding the sender's connection."""
    for dev in msg.recipient_devices:                 # sender already encrypted once per device
        conn = server.registry.lookup(msg.to_user, dev)
        if conn and conn.alive():
            conn.send(msg.envelope_for(dev))          # opaque ciphertext + routing header
            server.ack_to_sender(msg.id, dev, "server")
        else:
            server.offline.append(msg.to_user, dev, msg.envelope_for(dev), ttl_days=30)
            server.push.wake(msg.to_user, dev)        # no content in the push

Delivery is at-least-once: a socket can die after the server writes but before the client acknowledges, so the server redelivers anything not acknowledged and the client drops duplicates by message id. Queue entries are deleted only on the device's acknowledgement. WhatsApp's privacy policy states that undelivered messages are kept in encrypted form for up to 30 days and then deleted, so the queue is a bounded buffer, not an archive.

Multi-device: client fan-out

Before 2021, WhatsApp Web mirrored the phone: the phone held the only keys and relayed everything, so the laptop stopped working when the phone's battery died. The multi-device redesign gave every companion device its own identity key. WhatsApp's engineering post describes the delivery model as client fan-out: the sending client encrypts and sends the message N times, once for each of N devices, each over its own pairwise Signal session.

The alternative, one key shared by all of a user's devices, would mean copying private keys around, so losing one device would expose all of them. Client fan-out keeps keys local; its upload cost is negligible for text and avoided for media, where only the key is fanned out.

The new problem is trust in the device list. If the server could silently add a device to Bob's account, it would receive a copy of every message. WhatsApp's answer is that the primary phone signs each companion device it links, and clients verify the signed device list before encrypting to a device. When Bob links a laptop, Alice's client sees a changed device list and can show a security notification. Your design needs the same property: the authority to add a device must belong to a device the user holds, not to the server.

A newly linked device has no old sessions, so history must come end-to-end from the primary device.

Groups: Sender Keys and server fan-out

Pairwise encryption for a 1,024-member group, each with several devices, would mean thousands of encryptions and uploads per message. WhatsApp's whitepaper describes Sender Keys instead. Each member generates a sender key, a chain key plus a signing key, and distributes it once to every other member's devices over the existing pairwise sessions. After that, each group message is encrypted once with the sender's current chain key and signed. The client uploads one ciphertext, and the server fans it out to every member device.

This splits the cost sensibly. The sender pays the pairwise cost once per member device when keys are distributed. The server pays the per-message fan-out in bandwidth, which is what servers are good at. The trade-off is weaker post-compromise security than pairwise ratchets: the chain moves forward with a hash ratchet, so old keys cannot be recovered, but someone who steals a sender key can read that sender's future group messages until it changes. So membership changes must rotate keys: when someone leaves or is removed, every remaining member generates a new sender key and redistributes it, and a large group with frequent membership changes pays that cost repeatedly.

CaseSender encryptions per messageSender uploadServer work
1:1, recipient has 3 devices, sender has 24 (3 + own other device)4 small ciphertextsRoute 4 envelopes
Group, 200 members x 2 devices, steady state11 ciphertextFan out to about 400 devices
Same group, one member removed1 + key redistribution by each memberabout 398 small key messages per memberBurst of key traffic

Media: encrypt, upload once, send the key

Files do not go through the chat path. The whitepaper describes the sender generating a random 32-byte media key, deriving an AES-256 key, an IV and an HMAC-SHA256 key from it, encrypting the file with AES-CBC, appending a MAC, and uploading the ciphertext to a blob store. The sender then sends a normal end-to-end encrypted message containing the media key, the SHA-256 hash of the ciphertext and a pointer to the blob. Recipients download, verify the hash and MAC, and decrypt.

import os, hmac, hashlib
from cryptography.hazmat.primitives import padding
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
from cryptography.hazmat.primitives import hashes

def encrypt_media(plaintext: bytes):
    media_key = os.urandom(32)
    okm = HKDF(algorithm=hashes.SHA256(), length=80, salt=None,
               info=b"media-type-info").derive(media_key)      # info string is illustrative
    iv, aes_key, mac_key = okm[:16], okm[16:48], okm[48:80]
    padder = padding.PKCS7(128).padder()
    enc = Cipher(algorithms.AES(aes_key), modes.CBC(iv)).encryptor()
    ct = enc.update(padder.update(plaintext) + padder.finalize()) + enc.finalize()
    mac = hmac.new(mac_key, iv + ct, hashlib.sha256).digest()
    blob = ct + mac
    return media_key, hashlib.sha256(blob).digest(), blob     # key + hash go in the E2E message

This is a sketch of the pattern, not a byte-compatible client. Large files are uploaded once no matter how many devices or group members receive them, because only the 32-byte key is fanned out. The blob store holds only ciphertext and cannot recognise the same photo uploaded by two people, so content deduplication across users is impossible; forwarding can reuse the existing blob only because the forwarder already holds its key.

Failure modes and how the design absorbs them

  • Chat server crash. Every socket on it drops and clients reconnect elsewhere. The registry entries must expire or be overwritten on reconnect, or messages get routed to a dead server. Unacknowledged messages stay queued, so they are delayed, not lost.
  • Reconnect storms. A regional blip makes millions of phones reconnect at once; clients need jittered backoff and servers need handshake admission control.
  • Prekey exhaustion. A popular offline device can run out of one-time prekeys; sessions fall back to the signed prekey until the client reconnects and replenishes.
  • Session desync. A restored or reinstalled device loses its ratchet state and cannot decrypt messages sent with the old session. The client asks the sender to retry, and the sender rebuilds the session from a fresh bundle.
  • Queue expiry. A device offline for more than 30 days loses queued messages. That is a product decision, enforced by TTL, not a bug.
  • Device-list attacks. Without signed device lists, the server could add a ghost device. Signed companion lists and key transparency are the defences.
  • Group key churn. Frequent joins and leaves in large groups cause bursts of sender-key redistribution.

Trade-offs worth naming

End-to-end encryption moves cost and complexity to clients. The server is simpler and safer to operate, but features that need content, such as server-side search, spam classification on message bodies, cross-device history sync and content deduplication, either disappear or move on-device.

Store-and-forward with deletion on acknowledgement keeps storage small and limits what a breach exposes, but means the server is not a backup. Backups are a separate, optional system. Compare these choices with iMessage's architecture, which makes similar per-device choices, and WeChat's super-app design, which does not encrypt end-to-end by default and so can build server-side features WhatsApp cannot. For the generic building blocks without encryption, see designing a real-time chat system, and for queue semantics see message queues in system design.

What to do next

  1. Write down your delivery semantics: at-least-once with client deduplication by message id, deletion only on device acknowledgement, and a stated queue TTL.
  2. Model sessions per device, not per user, before writing any encryption code; draw the fan-out for a user with three devices.
  3. Make adding a device require a signature from a device the user already holds, and show recipients when a device list changes.
  4. Keep payloads out of push notifications; use them only as wake-up signals.
  5. Encrypt media on the client, upload once, and fan out only the key, hash and pointer.
  6. Load-test reconnect storms with jittered backoff and server admission control before launch.
  7. Track prekey inventory, offline queue depth and age, and decryption-retry rates as first-class metrics.
Key takeaway: WhatsApp's architecture follows from one rule: the server routes ciphertext it cannot read. That rule makes the chat tier a connection holder, router, bounded offline queue and key directory, and moves everything else to clients. Sessions are per device, so multi-device means client fan-out and signed device lists. Groups use Sender Keys so the server can fan out one ciphertext, and media is uploaded once while only its key is fanned out. Design the key directory and device trust carefully, because they are where an end-to-end system can quietly fail.