Design WhatsApp Messenger: Learn System Design from Scratch (2026)
WhatsApp is the world's largest messaging platform with 2B+ users sending 100B+ messages daily. The core challenge is reliable message delivery with end-to-end encryption at massive scale. I have designed messaging systems for millions of users and the key decisions are the persistence model, delivery guarantees, and the encryption protocol.
This tutorial covers the complete WhatsApp architecture: the message queue and persistence layer, end-to-end encryption with the Signal Protocol, the WebSocket-based real-time delivery, group chat with fanout, and the serverless approach (no message storage on server after delivery).
Requirements and Scale
WhatsApp has 2B+ users, 100B+ messages sent daily. Key operations: send message (text, image, video, audio, document), delivery receipt, read receipt, group messaging (up to 1024 members), voice/video calls, end-to-end encryption. Non-functional: 99.999% availability, message delivery within 1 second typically, support for 100M+ concurrent connections, zero message loss. Scale estimates: 100B messages/day = 1.15M messages/sec average, peak 5M+ msg/sec. Each message payload averages 500 bytes (text) to 10MB (video). Storage approach: WhatsApp does NOT store messages on server after delivery (except undelivered messages queued for 30 days). This is the fundamental architectural decision.
// WhatsApp scale\n// Users: 2B+\n// Daily messages: 100B+\n// Peak messages/sec: 5M+\n// Avg message size (text): 500 bytes\n// Avg message size (media): 200KB (thumbnail + compressed)\n// Concurrent connections: 100M+\n// Groups: hundreds of millions\n// Voice/video calls/day: 2B+ minutes\n// Engineering team (2014): 57 (acquired by Facebook at 450M users)\n// Servers: ~1000 (pre-Facebook infrastructure)
End-to-End Encryption (Signal Protocol)
WhatsApp uses the Signal Protocol for end-to-end encryption. Each client generates: an identity key pair (Curve25519 long-term), a signed pre-key (medium-term, rotated periodically), and a set of one-time pre-keys. When Alice wants to send Bob a message: (1) Alice's client fetches Bob's pre-key bundle from the server, (2) Alice performs X3DH (Extended Triple Diffie-Hellman) key agreement to establish a shared secret, (3) A root key is derived and used to initialise a Double Ratchet chain, (4) Each message uses a new message key derived from the ratchet, providing forward secrecy and break-in recovery. The server NEVER has access to message content. Server-side: stores pre-key bundles and delivers encrypted ciphertexts.
// Signal Protocol key exchange (simplified)\n// Alice fetches Bob's pre-key bundle:\nconst bobBundle = await server.getPreKeyBundle('bob');\n// bobBundle = { identityKey, signedPreKey, signature, oneTimePreKey }\n\n// X3DH key agreement:\nconst aliceIdentity = alice.identityKey;\nconst aliceEphemeral = generateKeyPair('curve25519');\nconst dh1 = aliceEphemeral.agree(bobBundle.identityKey);\nconst dh2 = aliceIdentity.agree(bobBundle.signedPreKey);\nconst dh3 = aliceEphemeral.agree(bobBundle.signedPreKey);\nconst dh4 = aliceEphemeral.agree(bobBundle.oneTimePreKey);\nconst sharedSecret = KDF(dh1 || dh2 || dh3 || dh4);\n\n// Initialize Double Ratchet\nconst ratchet = new DoubleRatchet(sharedSecret, bobBundle.signedPreKey);\nconst encryptedMessage = ratchet.encrypt('Hello Bob!');
Message Delivery and Persistence
WhatsApp's message delivery model: (1) Alice's client sends encrypted message to server via persistent WebSocket. (2) Server checks if Bob is connected (presence info in Redis). (3) If Bob is online: server forwards message to Bob via his WebSocket connection. Server does NOT persist the message after delivery. (4) If Bob is offline: server stores the encrypted message in a per-user message queue (Mnesia/MySQL) for up to 30 days. (5) When Bob comes online, his client receives all queued messages. (6) Bob's client sends delivery receipt back via the same path. The server is stateless regarding message content — it only stores routing metadata and the encrypted ciphertext. Each message has a globally unique ID for deduplication.
// Message delivery flow\n// Alice sends message via WebSocket\nws.send(JSON.stringify({\n type: 'SEND_MESSAGE',\n to: 'bob',\n messageId: 'msg_uuid_123',\n ciphertext: 'base64_encrypted...',\n senderKeyId: 2\n}));\n\n// Server receives and routes\napp.ws('/ws', (ws, req) => {\n ws.on('message', async (data) => {\n const msg = JSON.parse(data);\n const recipientWs = connectedClients.get(msg.to);\n if (recipientWs && recipientWs.readyState === WebSocket.OPEN) {\n recipientWs.send(JSON.stringify({\n type: 'NEW_MESSAGE',\n from: req.user.id,\n messageId: msg.messageId,\n ciphertext: msg.ciphertext\n }));\n } else {\n // Queue for offline delivery\n await queueOfflineMessage(msg.to, msg);\n }\n });\n});
Group Chat Architecture
WhatsApp supports groups up to 1024 members. Group messaging uses server-side fanout: when Alice sends a message to a group, the server receives it once and fans out to all group members. For small groups (<32 members), the server sends individual messages to each member. For large groups, the server sends the message once to the group's broadcast channel and connected members receive it. The group metadata (members, name, icon) is stored in a distributed database (Cassandra). Each group has a group session ID that is used for encryption: messages are encrypted with a group key (sender key) which is shared among group members using the Signal Protocol's sender key distribution.
// Group message fanout\nasync function sendGroupMessage(senderId, groupId, ciphertext, senderKeyId) {\n const members = await getGroupMembers(groupId);\n const message = {\n type: 'GROUP_MESSAGE',\n from: senderId,\n groupId,\n ciphertext,\n senderKeyId\n };\n for (const memberId of members) {\n if (memberId === senderId) continue;\n const memberWs = connectedClients.get(memberId);\n if (memberWs && memberWs.readyState === WebSocket.OPEN) {\n memberWs.send(JSON.stringify(message));\n } else {\n await queueOfflineMessage(memberId, message);\n }\n }\n}\n\n// Sender key distribution (group encryption)\n// Alice creates a sender key and distributes to group members:\n// Each member decrypts the sender key using their pairwise session key\n// Then group messages are encrypted with AES using the sender key\nconst senderKey = crypto.randomBytes(32); // Group session key\nconst encryptedSenderKey = aliceSession.encryptFor(bob, senderKey);\n// Bob receives and decrypts: aliceSenderKey = bobSession.decrypt(encryptedSenderKey)\n// All subsequent group messages use AES-GCM with sender key
Media and Voice Messages
Media messages (images, video, audio) are handled differently from text. The client uploads the encrypted media to a blob store (S3/FB's Haystack) with a random key. The server stores only the hash of the media. The message sent to the recipient contains: the encryption key, the blob URL (expiring), a thumbnail (if image/video), and media metadata. The thumbnail is also encrypted with the same message key. Voice messages are uploaded similarly and treated as audio media. The upload uses chunked transfer with resumable uploads. The server de-duplicates media: same file uploaded by two users stores only one blob (but each user has a separate encrypted copy if encryption is end-to-end).
// Media message upload\n// Client side:\nconst mediaKey = crypto.randomBytes(32);\nconst encryptedFile = await aesGcmEncrypt(fileBuffer, mediaKey);\nconst blobHash = crypto.createHash('sha256').update(encryptedFile).digest('hex');\nconst uploadUrl = await server.getUploadUrl(blobHash, encryptedFile.length);\nawait fetch(uploadUrl, { method: 'PUT', body: encryptedFile });\n\n// Send message with media reference\nws.send(JSON.stringify({\n type: 'SEND_MESSAGE',\n to: 'bob',\n messageId: 'msg_456',\n media: {\n url: `https://mmg.whatsapp.net/${blobHash}`,\n encKey: mediaKey.toString('base64'),\n sha256: blobHash,\n thumb: encryptedThumbnail.toString('base64'),\n mimeType: 'image/jpeg',\n fileSize: encryptedFile.length\n }\n}));
Presence and Status
WhatsApp shows online/last seen status. Presence is tracked via the WebSocket connection: when a user connects, their status is set to ONLINE. After disconnection, last seen is updated. The presence service uses Redis pub-sub: when a user's status changes, it publishes to subscribers (users who have the user in their contacts). The status update is rate-limited: broadcast to at most 100 contacts per change. For privacy, users can configure who can see their status (Everyone, My Contacts, Nobody). WhatsApp Status (stories) uses the same ephemeral storage pattern as Instagram Stories but with end-to-end encryption: status images are encrypted with a per-status key and uploaded to the blob store.
// Presence tracking\n// WebSocket connect handler\nws.on('connect', async () => {\n await redis.set(`presence:${userId}`, 'online');\n await redis.expire(`presence:${userId}`, 300); // heartbeat\n // Notify contacts\n const contacts = await getContacts(userId);\n for (const contactId of contacts) {\n await redis.publish(`presence_channel:${contactId}`, JSON.stringify({\n userId, status: 'online', timestamp: Date.now()\n }));\n }\n});\n\nws.on('close', async () => {\n const lastSeen = Date.now();\n await redis.set(`presence:${userId}`, 'offline');\n await redis.set(`last_seen:${userId}`, lastSeen);\n});
Scalability and Server Architecture
WhatsApp's server architecture (pre-Facebook) was famously minimal: ~1000 servers supporting 450M+ users with only 57 engineers. The stack: Ejabberd (Erlang XMPP server) customized for persistent connections, FreeBSD for the OS, and Mnesia (Erlang distributed DB) for session state. The key to scalability: each server handles 1M+ concurrent WebSocket connections using Erlang's lightweight processes. The server is stateless regarding message content — all routing decisions are based on connection state in memory. Horizontal scaling: users are assigned to a server based on a consistent hash of their phone number. Each server has a backup for failover. The message queue for offline users uses a per-user append-only log (Mnesia disc_copies).
// Connection routing with consistent hashing\nconst servers = ['wa1.erlang', 'wa2.erlang', 'wa3.erlang'];\nconst VIRTUAL_NODES = 100;\n\nfunction getServerForUser(phoneNumber) {\n const hash = murmur3(phoneNumber.toString());\n const ring = [];\n for (const server of servers) {\n for (let i = 0; i < VIRTUAL_NODES; i++) {\n ring.push({ hash: murmur3(server + ':' + i), server });\n }\n }\n ring.sort((a, b) => a.hash - b.hash);\n for (const node of ring) {\n if (hash <= node.hash) return node.server;\n }\n return servers[0];\n}
Frequently Asked Questions
How does WhatsApp's end-to-end encryption work?
WhatsApp uses the Signal Protocol: X3DH key agreement establishes a shared secret, then the Double Ratchet algorithm provides forward secrecy. Each message uses a unique key, so compromising one message doesn't reveal others.
Does WhatsApp store messages on its servers?
No, WhatsApp messages are not stored on servers after delivery. Undelivered messages are queued for up to 30 days. Once delivered, only metadata (delivery receipts) is retained briefly.
How does WhatsApp handle 100B+ messages per day?
WhatsApp uses Erlang/Elixir on FreeBSD, with each server handling 1M+ concurrent connections via lightweight Erlang processes. Users are assigned to servers via consistent hashing of phone numbers.
How does WhatsApp group chat work?
Group messages are sent once to the server, which fans out to all members. Groups up to 1024 members use sender key encryption (AES-GCM with a group session key shared via pairwise encrypted channels).
Originally published on Ayodhyyya. Last updated June 1, 2026.