?

Side quest 02Messaging

WhatsApp Messaging.

How 50 Engineers Built the World's Most Efficient Messaging Platform

INTERMEDIATE37 MIN READ6 PHASES+120 XP

Mission brief

Design a global messaging platform

Quest log0/7 stages · 0/5 checkpoints · 0/7 quick checks
messages a day (last official figure, Oct 2020)
100B+
messages per second at peak (our estimate)
~3M
delivery to a user who is online
< 500 ms
concurrent connections to design for
50M

Company names and logos are trademarks of their owners, shown only to identify each company. This course isn't affiliated with or endorsed by any of them.

watch it first · newThe whole design in a 16-minute animated lecture4 quiz checkpoints · ask the AI about any moment · captions, chapters and speed
Optional · timed run · 6 phases

Want it harder? Hide the model answer and work each phase in a scratchpad, then reveal and score yourself. Or simply read on: the checkpoints below work either way.

Intro · 2 min

The Impossible Made Simple

When Facebook acquired WhatsApp for $19 billion in 2014, something didn't add up.

The messaging app was handling about 50 billion messages daily — with only about 32 engineers (Wired later put the figure at 35), and roughly 10 of them on the Erlang backend.

How is that even possible?

WhatsApp delivery · one tick at the server, two at the phone, then the read receipt and a group fan-out
LIVEWhatsApp delivery · one tick at the server, two at the phone, then the read receipt and a group fan-out
The WhatsApp paradox

The WhatsApp paradox (2014 acquisition)

  • Messages per day: ~50 Billion (inbound + outbound)
  • Engineers: ~32 (Wired: 35; ~10 on the Erlang server) · ~55 employees total
  • Servers: ~550
  • Cost per user: Fraction of competitors
  • Connections per server: ~1 Million in production; 2M+ shown possible, 2.8M peak (Industry: ~100K typical)
  • Technology stack: Erlang + FreeBSD (Not Java, not Linux)

For comparison:

  • Facebook Messenger: engineering teams many times larger
  • Most messaging apps: far larger teams for similar scale

So how did about 32 engineers run it?

Today's scale (latest public figures)

  • Monthly active users: 3+ Billion (April 2025)
  • Daily messages: 100+ Billion (last official figure, Oct 2020)
  • Peak messages/second: ~3 Million (our estimate; not published)
  • Countries: 180+
  • Updates tab (Status + Channels) daily users: ~1.5 Billion (June 2025)
  • Voice/video calls: 2+ Billion minutes/day (2018, last published figure)
  • Encryption: End-to-End (Signal Protocol) — WhatsApp can't read messages
  • Availability target (this design): 99.99%

This is the system we'll design today — and understand why their technology choices matter.

ExplainerThe whole quest: WhatsApp

WhatsApp delivers 100 billion+ messages a day to 50 million connected phones: at 2 million connections a server that is 25 servers instead of 500, and deleting delivered messages shrinks 50 TB a day to ~2.5 TB. Erlang chat servers route encrypted blobs via a user registry with one tick, two ticks and blue ticks, while media goes to a separate store. The deep dives cover ~2 KB Erlang processes, the Signal Protocol, offline queues deleted only on ACK and presence with a 60 s TTL, then celebrity status, sender keys, linked devices and the 32-engineer lesson.

The Interview Begins#

You're interviewing at a startup that wants to build the next great messaging app. The CTO draws on the whiteboard:

Interviewer

"WhatsApp proved that a small team can build a messaging platform for billions. I want you to design a messaging system that can scale to that level. Walk me through how you'd approach it."

Design a Global Messaging Platform

Build a messaging system that can handle billions of users with:

Requirements
  • Support 2 billion users
  • Handle 100+ billion messages per day
  • Real-time message delivery (< 500ms for online users)
  • Offline message storage and delivery
  • End-to-end encryption (server cannot read messages)
  • Group messaging (up to 1024 members)
  • Media sharing (images, video, documents)
  • Presence (online/offline/typing indicators)
  • Message status (sent, delivered, read)
  • 99.99% availability

Constraint: Build it with a small team (< 100 engineers)

Interviewer

"The team size constraint is intentional. I want to see if you understand that architecture choices determine how many engineers you need. WhatsApp proved you can do more with less."

Phase 1 · Stage 1 of 7 · 2 min

Phase 1: Requirements Clarification#

You

"Let me make sure I understand the constraints and can make the right trade-offs."

Your Questions#

Transcript
You

"First, what's the message delivery guarantee? Is it okay if a message is delayed, or must it be truly real-time?"

Interviewer

"For online users, we want sub-second delivery. For offline users, messages should be queued and delivered when they come online. Messages should never be lost."

You

"What about message ordering? If I send two messages quickly, must they arrive in order?"

Interviewer

"Within a single conversation, yes — messages must be ordered. But I don't care if Message A to Bob arrives before Message B to Alice, even if B was sent first."

You

"For end-to-end encryption, does the server ever need to read message content?"

Interviewer

"Never. The server should only route encrypted blobs. This is non-negotiable for user trust."

You

"What's the read/write ratio? How often do users check for messages vs send them?"

Interviewer

"Heavy read. Users open the app frequently to check for messages. For every message sent, there are probably 10 message reads/checks. Also, presence updates are extremely chatty — typing indicators, online status."

You

"What about media? Video messages can be large."

Interviewer

"Media follows a different path. Upload to storage, send a reference in the message. The encrypted media is stored separately from chat servers."

Requirements Summary#

Functional Requirements:

Functional requirements
1. Messaging
  • One-to-one messaging
  • Group messaging (up to 1024 members)
  • Message types: text, image, video, audio, document, location
  • Message status: sent → delivered → read
  • Reply, forward, delete (for everyone)
2. Presence
  • Online/offline status
  • Last seen timestamp
  • Typing indicators
  • Privacy controls (hide last seen)
3. Media handling
  • Upload and download media
  • Automatic compression
  • Progressive loading (blurry → clear)
  • Media forwarding (reuse uploaded media)
4. Security
  • End-to-end encryption for all messages
  • Device verification (QR code scan)
  • Two-factor authentication
5. Sync
  • Multi-device support
  • Message sync across devices
  • Local backup (encrypted)

Non-Functional Requirements:

Non-functional requirements
Scale
  • 2 billion users
  • 500 million daily active users
  • 100 billion messages/day
  • 3 million messages/second (peak)
Latency
  • Online delivery: < 500ms
  • Offline delivery: < 5s after coming online
  • Typing indicators: < 200ms
Availability
  • 99.99% uptime
  • Graceful degradation (messages queue if delays)
Storage
  • Messages stored until delivered
  • Delivered messages deleted from server
  • Media retained for 30 days
Efficiency
  • Minimal bandwidth (works on 2G)
  • Minimal battery drain
  • Small engineering team

Phase 2 · Stage 2 of 7 · 2 min

Phase 2: Back of the Envelope Estimation#

You

"Let me work through the numbers to understand infrastructure needs."

Your turn · checkpoint 1not answered

Before the numbers: how many servers just to hold the connections?

500 million people use the app daily and about 10% are connected at the busiest moment. A typical server holds 50,000 to 100,000 persistent connections; a heavily tuned one can hold 2 million. How many connection servers do you need in each case?

Think it through, or jot it down. Then compare.

One strong answer

  • Connections: 500M × 10% = 50 million at peak.
  • Typical servers: 50M ÷ 100K = 500 servers (1,000 at 50K each).
  • Tuned servers: 50M ÷ 2M = 25 servers, 50 to 75 with redundancy.

Connection density, not message count, is what sets the size (and the staff) of a chat backend. That's why the runtime and kernel choices in the next phases matter so much.

Traffic Calculations#

WorksheetMessage volume
  • Daily messages: 100,000,000,000 (100 billion)
  • Seconds per day: 86,400
  • Average MPS: ~1.16 million messages/second
  • Peak multiplier: 3x (evening hours globally)
  • Peak MPS: ~3.5 million messages/second
  • Each message triggers:
    • Sender acknowledgment: 1 message
    • Recipient delivery: 1 message
    • Read receipt: 1 message
    • Total events/message: ~3-4x
  • Effective events/second: ~10-15 million at peak

Connection Calculations#

WorksheetConcurrent connections
  • Daily active users: 500 million
  • Peak concurrent: ~10% = 50 million connections
  • If each server handles 2 million connections:
    • Servers needed: 25 servers (just for connections!)
    • With redundancy: 50-75 servers
  • WhatsApp showed one server could hold over 2 million connections (2.8M peak), and ran ~1 million per chat server in production in 2014. Most companies: 50,000-100,000 per server. This 10-40x efficiency is the key insight.

Storage Calculations#

WorksheetMessage storage
  • Average message size: ~500 bytes (encrypted)
  • Daily messages: 100 billion
  • Daily storage: 50 TB/day (if we stored everything)
  • BUT: WhatsApp deletes messages after delivery!
    • Undelivered at any time: ~5% of daily messages
    • Actual storage: ~2.5 TB active
    • Plus offline queues: ~10 TB
  • Media storage is separate:
    • Daily uploads: ~10 billion media files
    • Average size: 100 KB (compressed)
    • Daily media: ~1 PB/day
    • 30-day retention: ~30 PB
    • With deduplication: ~15 PB
Quick check3 questions · +5 XP each

Requirements and estimation

  1. Q1/3 Peak is ~50 million concurrent connections. If your servers held the industry-typical 100,000 connections each instead of 2 million, how many connection servers would you need before redundancy?

    Answer & explanation

    Answer: C

    50M / 100K = 500 servers, versus 50M / 2M = 25 at WhatsApp's density. That 20x gap is why per-server connection efficiency is the key insight of the estimate, not a detail.

  2. Q2/3 Storing every message would take ~50 TB/day, yet active message storage is only a few TB. Why?

    Answer & explanation

    Answer: A

    The server is a delivery queue, not an archive: delivered messages are deleted, leaving roughly 5% undelivered at any time. Media does go to separate storage, but that is not what shrinks message storage from 50 TB/day.

  3. Q3/3 The interviewer requires ordering only within a conversation, not across conversations. What does that free you from?

    Answer & explanation

    Answer: D

    Per-conversation ordering can be enforced locally for each chat; a global order across 100 billion messages a day would need costly system-wide coordination. Acks, offline storage and encryption are all still required; ordering scope does not touch them.

Phase 3 · Stage 3 of 7 · 3 min

Phase 3: High-Level Architecture#

You

"WhatsApp's architecture is remarkably simple. That's the secret — simplicity enables a small team."

Your turn · checkpoint 2not answered

Sketch it first: what does the server actually keep?

Messages are end-to-end encrypted. What does the server need to know to deliver one, what does it store, and for how long? How does the sender learn the message arrived?

Think it through, or jot it down. Then compare.

One strong answer

  • Knows: routing metadata only: sender, recipient, time, size. The body is an encrypted blob it cannot open.
  • Stores: the blob only until the recipient's device acknowledges it, then deletes it. Undelivered messages wait in the recipient's offline queue.
  • Media: uploaded encrypted to separate storage; the message carries a reference, so chat servers never carry large payloads.
  • Status: one tick when the server has accepted the message, two when the recipient's device has it, then a read receipt, each sent back as a small event.

Storing so little is what keeps the backend cheap: there is no growing archive of everyone's chats to shard, back up and protect.

The WhatsApp Philosophy#

WhatsApp's engineering principles
1. Focus
  • One thing, done exceptionally well: messaging.
  • No social feed, no stories (initially), no ads (ads arrived only in June 2025, in the Updates tab).
  • Feature creep is the enemy of reliability.
2. Reliability over features
  • A message that doesn't deliver is worse than no feature.
  • Every new feature adds complexity and potential failures.
  • Say no to most feature requests.
3. Right tool for the job
  • Erlang for concurrency (designed for telecom switches).
  • FreeBSD for networking performance.
  • Mnesia for in-memory distributed data.
  • Unconventional, but perfect fit.
4. Small team = better communication
  • 32 engineers means everyone knows the whole system.
  • Founders reportedly close to the code in the early years (anecdotal, not documented).
  • No coordination overhead between 50 microservice teams.
5. Let it crash
  • Erlang philosophy: processes crash, supervisors restart.
  • Don't try to handle every error — let it fail and recover.
  • Simple, predictable failure recovery.

System Architecture#

DiagramA likely WhatsApp-style architecture
%% title: A likely WhatsApp-style architecture
flowchart TD
  subgraph CL["Client layer"]
    direction TB
    AN@{ icon: "tech:android", label: "Android App" }
    IO@{ icon: "tech:apple", label: "iOS App" }
    WW@{ icon: "sd:browser", label: "WhatsApp Web" }
  end
  subgraph EG["Edge / gateway"]
    direction TB
    LB@{ icon: "sd:load-balancer", label: "LOAD BALANCERS<br/>GeoDNS routes to nearest data center" }
    subgraph CS["Chat servers (Erlang + FreeBSD)"]
      direction TB
      C1["Chat 1 · ~1-2M conns"]:::core
      C2["Chat 2 · ~1-2M conns"]:::core
      C3["Chat 3 · ~1-2M conns"]:::core
      CN["Chat N · ~1-2M conns"]:::core
    end
    LB ==> CS
  end
  CL ==>|"Persistent TCP · Noise Pipes encryption<br/>custom binary protocol"| LB
  subgraph BE["Backend services · Mnesia (Erlang distributed DB) + ETS (in-memory)"]
    direction TB
    MR["MESSAGE ROUTING<br/>Direct P2P between chat servers"]:::service
    OS@{ icon: "sd:queue", label: "OFFLINE STORE<br/>Queue for offline users" }
    MS@{ icon: "sd:object-storage", label: "MEDIA STORAGE<br/>S3-like blob store for media" }
    UR@{ icon: "sd:database", label: "USER REGISTRY<br/>User→Server mapping" }
    GM@{ icon: "sd:database", label: "GROUP METADATA<br/>Group membership" }
    KS@{ icon: "sd:key", label: "KEYS (for E2E)<br/>Public key distribution" }
    MR ~~~ UR
    OS ~~~ GM
    MS ~~~ KS
  end
  CS --> BE
  class AN,IO,WW client
  class LB edge
  class OS queue
  class MS,UR,GM,KS store
  • Each connection = 1 Erlang process (a bare process starts at ~2.7 KB; real ones grow with buffers and session state).
  • Each process handles: auth, presence, message routing.
  • All backend services are backed by Mnesia (Erlang distributed DB) + ETS (in-memory).

Message Flow#

DiagramMessage flow — Alice sends "Hello" to Bob
%% title: Message flow — Alice sends "Hello" to Bob
sequenceDiagram
  autonumber
  participant A as Alice's phone
  participant S as WhatsApp servers
  participant B as Bob's phone
  A->>A: ENCRYPT · with a per-message key from the Signal session with Bob
  A->>S: SEND · encrypted blob + recipient: bob@wa
  S-->>A: ACK (SENT) · server received message (single tick)
  S->>S: LOOKUP · where is Bob connected? User registry → Server 7
  opt IF BOB IS ONLINE
    S->>B: ROUTE · forward to Bob's process
    B-->>S: DELIVERED ACK · Bob's device got it
    S-->>A: DELIVERED (double tick)
  end
  opt BOB READS THE MESSAGE
    B-->>S: READ RECEIPT · Bob opened the chat
    S-->>A: READ (blue double tick)
  end
Quick check3 questions · +5 XP each

High-level architecture

  1. Q1/3 Alice's message shows a single tick for hours. What does that tell her?

    Answer & explanation

    Answer: B

    A single tick is the server's ACK; the double tick only comes after Bob's device sends a delivered ACK, typically when he comes online. Read status is a separate, later step (the blue ticks), so a single tick says nothing about reading.

  2. Q2/3 Alice is connected to one chat server and Bob to another. How does Alice's server know where to send the message?

    Answer & explanation

    Answer: C

    The user registry maps each user to their chat server, so the message goes straight to Bob's process. GeoDNS only picks a data center for incoming client connections; broadcasting to every server would not scale.

  3. Q3/3 Why is media uploaded to a separate blob store, with only a reference sent through the chat servers?

    Answer & explanation

    Answer: D

    Chat servers are tuned for millions of small, chatty connections; pushing videos through them would choke that path. The message carries just a reference to the encrypted media. Media is still end-to-end encrypted, and the server never decrypts it.

Phase 4 · Stage 4 of 7 · 17 min

Phase 4: Deep Dives#

Deep Dive 1: Why Erlang?#

Week 2 concepts: Failure handling, resilience. Week 10 concepts: Operational simplicity.

You

"WhatsApp's secret weapon is Erlang — a language designed in the 1980s for telephone switches. It's a perfect fit for messaging."

The Challenge:

The concurrency problem

Traditional approach (one OS thread per connection)

  • Each connection = 1 OS thread
  • OS thread = ~1MB of stack reserved (often more)
  • Context switching is expensive
  • 1 million connections = 1 million threads = ~1TB of stack reserved!
  • Practical limit: ~50,000-100,000 connections/server

WhatsApp needs

  • 1-2 million connections per server
  • Each connection needs state (session, queue)
  • Must handle failures gracefully
  • Hot code reloading (no restarts for deploys)

Why Erlang Solves This:

ERLANG'S SUPERPOWERS

1. LIGHTWEIGHT PROCESSES

Erlang processes are NOT OS threads. They're managed by the BEAM virtual machine.

OS Thread Erlang Process
~1MB stack reserved ~2.7KB at spawn
Expensive switch Cheap switch
Limited count Millions possible

WhatsApp runs 1 Erlang process per user connection. The bare processes for 2 million connections take only ~5GB; with socket buffers and session state, WhatsApp's January 2012 server held 2.28 million connections with roughly 27GB of its ~103GB of RAM still free (WhatsApp blog, January 2012). A thread per connection would reserve ~2TB of stack before doing any work.

2. "LET IT CRASH" PHILOSOPHY

  • Traditional: Try to handle every error (complex, error-prone)
  • Erlang: Let processes crash, supervisors restart them
DiagramLet it crash — supervisor restarts a crashed process
%% title: Let it crash — supervisor restarts a crashed process
flowchart TD
  SUP["SUPERVISOR<br/>monitors children"]:::core
  UA["User A Process"]:::service
  UB["User B Process"]:::bad
  UC["User C Process"]:::service
  UB2["User B Process<br/>Supervisor restarts in milliseconds"]:::ok
  SUP --> UA & UB & UC
  UB -->|"CRASH!"| UB2

User A and C never notice. User B reconnects automatically.

3. HOT CODE RELOADING

Deploy new code WITHOUT restarting the server. Connections stay alive during deployments. No maintenance windows needed.

4. DISTRIBUTED BY DESIGN

Erlang processes can communicate across machines seamlessly. Send a message to a process on another server = same syntax. Built-in distribution, no external message queue needed.

WhatsApp's Erlang Customizations:

pythonUserConnectionProcess · 92 lines
# Pseudocode: How WhatsApp structures connection handling
# (Actual WhatsApp code is Erlang, this shows the concept)

"""
WhatsApp Connection Handler

Each user connection runs as a single Erlang process.
This process:
1. Owns the TCP socket
2. Maintains user session state
3. Routes messages to/from the user
4. Dies cleanly when user disconnects
"""

class UserConnectionProcess:
    """
    Conceptual model of WhatsApp's per-user Erlang process.
    
    In Erlang, this is implemented with gen_server behavior.
    Memory: a few KB per process at spawn
    Lifecycle: Lives as long as user is connected
    """
    
    def __init__(self, socket, user_id):
        self.socket = socket
        self.user_id = user_id
        self.session_key = None
        self.message_queue = []  # For messages while processing
        self.last_seen = now()
        
        # Register this process in the global user registry
        UserRegistry.register(user_id, self.pid)
    
    def handle_message(self, msg):
        """
        Main message handler — called for every incoming message.
        
        Erlang processes have a mailbox — messages queue up
        and are processed one at a time.
        """
        match msg:
            case AuthRequest(credentials):
                self.handle_auth(credentials)
            
            case OutgoingMessage(recipient, encrypted_content):
                self.route_to_recipient(recipient, encrypted_content)
            
            case IncomingMessage(sender, encrypted_content):
                self.deliver_to_socket(sender, encrypted_content)
            
            case PresenceUpdate(status):
                self.broadcast_presence(status)
            
            case Disconnect():
                self.cleanup_and_die()
    
    def route_to_recipient(self, recipient_id, content):
        """
        Route message to another user.
        
        This is the core of WhatsApp's routing:
        1. Look up recipient's process (which server?)
        2. Send message directly to that process
        """
        # Find recipient's process (could be on any server)
        recipient_process = UserRegistry.lookup(recipient_id)
        
        if recipient_process:
            # Direct process-to-process communication
            # Works across servers in Erlang!
            send(recipient_process, IncomingMessage(
                sender=self.user_id,
                content=content
            ))
        else:
            # User offline — store for later
            OfflineStore.queue(recipient_id, content)
        
        # Server has the message: single tick (SENT). DELIVERED is
        # sent only when the recipient's DEVICE acks receipt.
        self.send_ack(MessageStatus.SENT)
    
    def cleanup_and_die(self):
        """
        Clean shutdown when user disconnects.
        
        In Erlang, when a process dies, all its memory
        is automatically reclaimed. No per-connection cleanup code needed.
        """
        UserRegistry.unregister(self.user_id)
        close(self.socket)
        # Process simply terminates — Erlang handles cleanup

The "Island" Architecture:

To limit blast radius of failures, WhatsApp partitioned its backend into small independent "islands" (in 2014, pairs of Mnesia database nodes that each held one slice of the data and replicated within the pair). The diagram scales the same idea up to groups of chat servers.

DiagramWhatsApp's island architecture
%% title: WhatsApp's island architecture
flowchart TD
  subgraph I1["Island 1"]
    S1["Chat Servers 1, 2, 3, 4<br/>Users A-H<br/>Replica: 1a"]:::core
  end
  subgraph I2["Island 2"]
    S2["Chat Servers 5, 6, 7, 8<br/>Users I-P<br/>Replica: 2a"]:::core
  end
  subgraph I3["Island 3"]
    S3["Chat Servers 9, 10, 11<br/>Users Q-Z<br/>Replica: 3a"]:::core
  end
  X["Cross-Island Routing<br/>for A→Z messages"]:::edge
  S1 & S2 & S3 --> X

BENEFITS:

  • Island 1 can fail without affecting Island 2
  • Upgrades can be rolled out island by island
  • Each island has its own replica for failover
  • Limits "blast radius" of any failure

Deep Dive 2: End-to-End Encryption (Signal Protocol)#

Week 9 concepts: Security, encryption.

You

"WhatsApp uses the Signal Protocol for end-to-end encryption. This is critical — even WhatsApp can't read your messages."

DiagramEnd-to-end encryption overview
%% title: End-to-end encryption overview
flowchart TD
  subgraph NO["Without E2EE"]
    direction LR
    A1@{ icon: "sd:user", label: "Alice" } -->|"#quot;Hi#quot;"| S1["Server<br/>Server can read #quot;Hi#quot;<br/>Hackers who breach server can read #quot;Hi#quot;<br/>Government subpoenas can access #quot;Hi#quot;"]:::bad
    S1 -->|"#quot;Hi#quot;"| B1@{ icon: "sd:user", label: "Bob" }
  end
  subgraph YES["With E2EE"]
    direction LR
    A2@{ icon: "sd:user", label: "Alice" } -->|"encrypted blob"| S2["Server<br/>Server sees random bytes<br/>Only Alice and Bob can decrypt<br/>Even WhatsApp can't read it"]:::ok
    S2 -->|"encrypted blob"| B2@{ icon: "sd:user", label: "Bob" }
  end
  NO ~~~ YES
  class A1,B1,A2,B2 client

Signal Protocol Components:

SIGNAL PROTOCOL: THE BUILDING BLOCKS

1. KEY PAIRS — each user has multiple key pairs:

  • Identity Key (long-term): Generated once when you install WhatsApp · Never changes (unless you reinstall) · Public part uploaded to WhatsApp servers
  • Signed Pre-Key (medium-term): Rotates periodically · Signed by Identity Key · Uploaded to servers
  • One-Time Pre-Keys (ephemeral): Batch of ~100 keys uploaded · Each used once then discarded · Provides forward secrecy

2. X3DH KEY AGREEMENT — when Alice messages Bob for the first time:

DiagramX3DH key agreement
%% title: X3DH key agreement
sequenceDiagram
  autonumber
  participant A as Alice
  participant S as Server
  participant B as Bob
  A->>S: "Give me Bob's keys"
  S-->>A: Bob's Identity Key, Signed Pre-Key, One-Time Pre-Key
  Note over A,B: Alice computes shared secret using:<br/>Her Identity Key + Bob's Signed Pre-Key<br/>Her Ephemeral Key + Bob's Identity Key<br/>Her Ephemeral Key + Bob's Signed Pre-Key<br/>Her Ephemeral Key + Bob's One-Time Pre-Key (if available)<br/>→ Shared Secret (only Alice and Bob can compute)

3. DOUBLE RATCHET — after initial key agreement, keys evolve with EVERY message:

  • Message 1: Encrypt with Key_1
  • Message 2: Encrypt with Key_2 (derived one-way from the chain key that produced Key_1)
  • Message 3: Encrypt with Key_3 (derived one-way from the next chain key)
  • ...

Benefits:

  • Forward Secrecy: Compromise of Key_3 doesn't reveal Msg 1-2
  • Post-Compromise Security: If a key leaks, the DH ratchet (fresh Diffie-Hellman keys exchanged as replies flow) makes later keys safe again
  • Break-in Recovery: System "heals" itself once a new DH ratchet step happens
pythonKeyPair · 204 lines
# encryption/signal_protocol.py

"""
Simplified illustration of Signal Protocol concepts.

The actual implementation uses:
- Curve25519 for key exchange
- AES-256-CBC for encryption
- HMAC-SHA256 for authentication
"""

from dataclasses import dataclass
from typing import Optional, Tuple
import os
import hashlib
import hmac


@dataclass
class KeyPair:
    """A public/private key pair."""
    public_key: bytes
    private_key: bytes


@dataclass
class SessionState:
    """State for an encrypted session with another user."""
    their_identity_key: bytes
    their_signed_prekey: bytes
    
    # Ratchet state
    root_key: bytes
    chain_key: bytes
    message_number: int = 0


class SignalSession:
    """
    Simplified Signal Protocol session.
    
    This illustrates the key concepts — actual implementation
    is more complex with proper cryptographic primitives.
    """
    
    def __init__(
        self,
        my_identity: KeyPair,
        their_identity_public: bytes,
        their_signed_prekey: bytes,
        their_one_time_prekey: Optional[bytes] = None
    ):
        self.my_identity = my_identity
        
        # Perform X3DH key agreement
        self.session = self._x3dh_key_agreement(
            their_identity_public,
            their_signed_prekey,
            their_one_time_prekey
        )
    
    def _x3dh_key_agreement(
        self,
        their_identity: bytes,
        their_signed_prekey: bytes,
        their_one_time_prekey: Optional[bytes]
    ) -> SessionState:
        """
        X3DH: Extended Triple Diffie-Hellman.
        
        Creates a shared secret from multiple DH exchanges.
        """
        # Generate ephemeral key for this session
        ephemeral = self._generate_keypair()
        
        # Perform multiple DH exchanges
        dh1 = self._dh(self.my_identity.private_key, their_signed_prekey)
        dh2 = self._dh(ephemeral.private_key, their_identity)
        dh3 = self._dh(ephemeral.private_key, their_signed_prekey)
        
        if their_one_time_prekey:
            dh4 = self._dh(ephemeral.private_key, their_one_time_prekey)
            shared_secret = self._kdf(dh1 + dh2 + dh3 + dh4)
        else:
            shared_secret = self._kdf(dh1 + dh2 + dh3)
        
        # Derive initial root and chain keys
        root_key, chain_key = self._derive_keys(shared_secret)
        
        return SessionState(
            their_identity_key=their_identity,
            their_signed_prekey=their_signed_prekey,
            root_key=root_key,
            chain_key=chain_key
        )
    
    def encrypt(self, plaintext: bytes) -> Tuple[bytes, int]:
        """
        Encrypt a message using current chain key.
        
        Each message gets a unique key (Double Ratchet).
        """
        # Derive message key from chain key
        message_key, new_chain_key = self._ratchet_chain(
            self.session.chain_key
        )
        
        # Encrypt with message key
        ciphertext = self._aes_encrypt(message_key, plaintext)
        
        # Update chain key (ratchet forward)
        message_number = self.session.message_number
        self.session.chain_key = new_chain_key
        self.session.message_number += 1
        
        return ciphertext, message_number
    
    def decrypt(self, ciphertext: bytes, message_number: int) -> bytes:
        """
        Decrypt a message.
        
        May need to skip ahead if messages arrive out of order.
        """
        # Derive the message key for this message number
        message_key = self._get_message_key(message_number)
        
        # Decrypt
        plaintext = self._aes_decrypt(message_key, ciphertext)
        
        return plaintext
    
    def _ratchet_chain(self, chain_key: bytes) -> Tuple[bytes, bytes]:
        """
        Symmetric ratchet: derive next keys from current.
        
        This is the "forward secrecy" mechanism — old keys
        can't be derived from new keys.
        """
        # Message key for current message
        message_key = hmac.new(chain_key, b"\x01", hashlib.sha256).digest()
        
        # New chain key for next message
        new_chain_key = hmac.new(chain_key, b"\x02", hashlib.sha256).digest()
        
        return message_key, new_chain_key


class GroupEncryption:
    """
    Group messaging uses a different approach.
    
    Instead of pairwise encryption, sender distributes
    a "sender key" to all group members.
    """
    
    def __init__(self, group_id: str, my_user_id: str):
        self.group_id = group_id
        self.my_user_id = my_user_id
        
        # Generate my sender key for this group
        self.my_sender_key = self._generate_sender_key()
        
        # Store other members' sender keys
        self.member_sender_keys = {}
    
    def distribute_sender_key(self, member_sessions: dict):
        """
        Send my sender key to all group members.
        
        Uses pairwise Signal sessions for secure distribution.
        """
        for member_id, session in member_sessions.items():
            # Encrypt sender key using pairwise session
            encrypted_key = session.encrypt(self.my_sender_key)
            # Send to member via normal message path
            yield member_id, encrypted_key
    
    def encrypt_for_group(self, plaintext: bytes) -> bytes:
        """
        Encrypt message using my sender key.
        
        All group members who have my sender key can decrypt.
        Much more efficient than pairwise encryption.
        """
        # Use sender key with chain ratchet
        message_key = self._derive_message_key()
        ciphertext = self._aes_encrypt(message_key, plaintext)
        
        # Ratchet forward
        self._ratchet_sender_key()
        
        return ciphertext
    
    def handle_member_left(self, member_id: str):
        """
        When someone leaves, all sender keys must be regenerated.
        
        This ensures the departed member can't read future messages.
        """
        # Generate new sender key
        self.my_sender_key = self._generate_sender_key()
        
        # Redistribute to remaining members
        # (handled by caller)

What WhatsApp CAN See (Metadata):

Encryption scope

Encrypted (WhatsApp cannot read)

  • Message content
  • Media files (photos, videos, documents)
  • Voice notes
  • Voice and video call content
  • Status updates

Not encrypted (WhatsApp can see)

  • Who messaged whom (sender/recipient)
  • When messages were sent
  • Message size
  • Device information
  • Phone numbers
  • Profile photos
  • Group membership
  • Online/offline status

This metadata is necessary for routing but reveals communication patterns even if content is secret.

Quick check3 questions · +5 XP each

Erlang and end-to-end encryption

  1. Q1/3 Why can one Erlang server hold around 2 million connections when a thread-per-connection server tops out near 100,000?

    Answer & explanation

    Answer: B

    WhatsApp's 2012 server held 2.28 million connections in roughly 70GB of RAM, a few tens of KB each including buffers; 2 million OS threads would reserve about 2TB of stack, plus expensive context switches. FreeBSD's network stack helps, but memory per connection is what sets the ceiling.

  2. Q2/3 An attacker steals the message key for message 3 in an ongoing chat. Can they decrypt messages 1 and 2?

    Answer & explanation

    Answer: C

    The ratchet derives each new key with a one-way function, so a later key reveals nothing about earlier ones. That is forward secrecy. Server deletion is irrelevant here: anyone holding ciphertext from earlier would still be protected by the key derivation, not by deletion.

  3. Q3/3 A government subpoenas WhatsApp for a user's chats. With E2EE in place, what can WhatsApp actually provide?

    Answer & explanation

    Answer: D

    Content is encrypted end to end; routing still needs sender, recipient, time and size, so that metadata is visible. Servers only hold public keys, which cannot decrypt anything.

Deep Dive 3: Offline Message Handling#

Week 3 concepts: Message queuing, delivery guarantees.

You

"Most users aren't online all the time. Offline message storage is critical."

Your turn · checkpoint 3not answered

Bob's phone was off for two days

Alice sent Bob 40 messages while his phone was off. He comes back on a flaky network that drops twice during the sync. How do you get all 40 to him, in order, with none missing and none shown twice?

Think it through, or jot it down. Then compare.

One strong answer

  • Queue per recipient: the 40 messages wait in Bob's offline queue, each with a message ID and a sequence number within the conversation.
  • Delete only on ack: the server removes a message only after Bob's device confirms it. If the connection drops mid-sync, the unacknowledged ones are sent again (at-least-once).
  • Dedup and order on the device: the client ignores IDs it already has and orders by the per-conversation sequence, so Bob sees each message once and in order.
  • Close the loop: delivery receipts flow back to Alice as the messages land.
  • Bound the queue: undelivered messages expire after 30 days, so a phone that never returns can't grow a queue forever.
DiagramOffline message flow — Alice sends a message to offline Bob
%% title: Offline message flow — Alice sends a message to offline Bob
sequenceDiagram
  autonumber
  participant A as Alice
  participant W as WhatsApp
  participant Q as Bob's offline queue
  participant B as Bob
  Note over B: offline
  A->>W: Send (encrypted)
  W-->>A: ACK (single tick) · "Message received by server"
  W->>Q: Store in Offline Queue (Msg 1, Msg 2, ...)
  Note over A,B: ... TIME PASSES (Bob offline) ...
  B->>W: Bob comes online · TCP connection
  W->>B: Drain queue · send all pending msgs
  B-->>W: Delivery ACK
  W-->>A: Delivered (double tick)
  W->>Q: Delete from queue (delivered)
ExplainerStore and forward: ticks, offline queue, delete on ACK

The server only routes Alice's encrypted blob: one tick when it ACKs, two when Bob's device ACKs delivery, blue ticks on the read receipt. If Bob is offline the message waits in his offline queue (kept up to 30 days), is drained when he reconnects, resent if the link drops before his ACK, de-duplicated by message ID on his device, and deleted only after that ACK.

pythonQueuedMessage · 165 lines
# offline/message_store.py

"""
Offline Message Storage

WhatsApp's approach:
- Messages stored only until delivered
- After delivery confirmation, deleted from server
- This minimizes server storage and privacy exposure
"""

import asyncio
from dataclasses import dataclass
from typing import List, Optional
from datetime import datetime, timedelta


@dataclass
class QueuedMessage:
    """A message waiting for delivery."""
    message_id: str
    sender_id: str
    recipient_id: str
    encrypted_content: bytes
    timestamp: datetime
    expiry: datetime  # Messages expire after 30 days
    
    # Delivery tracking
    delivery_attempts: int = 0
    last_attempt: Optional[datetime] = None


class OfflineMessageStore:
    """
    Stores messages for offline users.
    
    Design principles:
    - In-memory for speed (Mnesia ETS tables)
    - Replicated for durability
    - Partitioned by recipient for scalability
    - Messages deleted after delivery
    """
    
    def __init__(self, db, max_queue_size: int = 10000):
        self.db = db  # Mnesia in actual WhatsApp
        self.max_queue_size = max_queue_size
        
        # TTL for stored messages
        self.message_ttl = timedelta(days=30)
    
    async def queue_message(
        self,
        sender_id: str,
        recipient_id: str,
        encrypted_content: bytes
    ) -> QueuedMessage:
        """
        Store message for offline recipient.
        """
        message = QueuedMessage(
            message_id=generate_id(),
            sender_id=sender_id,
            recipient_id=recipient_id,
            encrypted_content=encrypted_content,
            timestamp=datetime.utcnow(),
            expiry=datetime.utcnow() + self.message_ttl
        )
        
        # Check queue size limits
        queue_size = await self.db.count_for_recipient(recipient_id)
        if queue_size >= self.max_queue_size:
            # Drop oldest message (or reject new one)
            await self.db.delete_oldest(recipient_id)
        
        await self.db.save(message)
        return message
    
    async def get_pending_messages(
        self,
        recipient_id: str
    ) -> List[QueuedMessage]:
        """
        Get all pending messages for a user who just came online.
        
        Called when user establishes connection.
        """
        messages = await self.db.get_by_recipient(
            recipient_id,
            order_by="timestamp",
            limit=1000  # Batch size
        )
        
        # Filter expired messages
        now = datetime.utcnow()
        valid_messages = [m for m in messages if m.expiry > now]
        
        return valid_messages
    
    async def mark_delivered(
        self,
        message_ids: List[str]
    ):
        """
        Delete messages that have been delivered.
        
        This is called after receiving delivery ACK from recipient.
        """
        for message_id in message_ids:
            await self.db.delete(message_id)
    
    async def cleanup_expired(self):
        """
        Background job: Delete expired messages.
        
        Runs periodically to clean up messages that
        were never delivered (user never came back online).
        """
        now = datetime.utcnow()
        expired = await self.db.get_expired(now)
        
        for message in expired:
            await self.db.delete(message.message_id)
            
            # Optionally notify sender that message expired
            # (WhatsApp doesn't do this)


class WriteBackCache:
    """
    WhatsApp uses write-back caching for offline messages.
    
    - Messages stored in memory first (fast)
    - Asynchronously persisted to disk
    - Handles disk I/O slowdowns gracefully
    """
    
    def __init__(self, memory_store, disk_store):
        self.memory = memory_store   # ETS table
        self.disk = disk_store       # Mnesia disk table
        self.pending_writes = []
    
    async def store(self, message: QueuedMessage):
        """
        Store in memory immediately, persist async.
        """
        # Immediate: memory storage
        await self.memory.put(message.message_id, message)
        
        # Async: queue for disk persistence
        self.pending_writes.append(message)
        
        # Trigger background write (non-blocking)
        asyncio.create_task(self._flush_writes())
    
    async def _flush_writes(self):
        """
        Background: persist to disk.
        
        Batched for efficiency.
        """
        if len(self.pending_writes) >= 100:
            batch = self.pending_writes[:100]
            self.pending_writes = self.pending_writes[100:]
            
            await self.disk.batch_write(batch)

Deep Dive 4: Presence and Typing Indicators#

Week 4 concepts: Caching, real-time updates.

You

"Presence is the 'chattiest' part of the system — far more traffic than messages."

Your turn · checkpoint 4not answered

Presence without a storm

Presence events ("online", "typing…", last seen) outnumber messages roughly ten to one. How do you keep presence from costing more than messaging itself?

Think it through, or jot it down. Then compare.

One strong answer

  • Memory only: presence lives in an in-memory store with short time-to-live heartbeats. Nothing about it is durably written.
  • Only to people who are looking: send status to users who have that chat open right now, not to every contact in someone's address book.
  • Fire and forget: typing indicators are throttled and debounced; losing one is harmless, so no acks and no retries.
  • Stale is fine: "last seen" can lag by seconds, so presence is eventually consistent by design.
  • Keep it off the message path: a presence surge must never delay a real message.
Presence traffic analysis

For every message sent, there are ~10-100 presence events:

  • User opens app (online)
  • User starts typing (typing indicator)
  • User stops typing
  • User reads messages (last seen update)
  • User closes app (offline)
  • Repeated for every chat participant watching
With 500M daily active users
  • Messages: ~100 billion/day
  • Presence events: ~1 trillion/day
  • This is 10x the message traffic!

Presence CANNOT go through the same path as messages. It needs aggressive optimization.

pythonPresenceState · 179 lines
# presence/manager.py

"""
Presence Management

Optimizations a WhatsApp-scale presence system needs (a likely design):
1. Batching: Aggregate multiple presence updates
2. Rate limiting: Don't send typing every keystroke
3. TTL: Presence expires automatically
4. Selective delivery: Only send to active chats
"""

from dataclasses import dataclass
from typing import Dict, Set, Optional
from datetime import datetime, timedelta
from enum import Enum


class PresenceState(Enum):
    ONLINE = "online"
    OFFLINE = "offline"
    TYPING = "typing"


@dataclass
class UserPresence:
    """Current presence state for a user."""
    user_id: str
    state: PresenceState
    last_seen: datetime
    typing_in_chat: Optional[str] = None


class PresenceManager:
    """
    Manages presence state and notifications.
    
    Key optimizations:
    1. In-memory only (no persistence needed)
    2. TTL-based expiry (online expires in 60s)
    3. Rate limiting (typing indicator throttled)
    4. Subscription-based (only notify interested parties)
    """
    
    def __init__(self, redis_cluster, notification_service):
        self.redis = redis_cluster
        self.notifier = notification_service
        
        # TTLs
        self.online_ttl = timedelta(seconds=60)
        self.typing_ttl = timedelta(seconds=5)
    
    async def set_online(self, user_id: str):
        """
        Mark user as online.
        
        Uses Redis with TTL — automatically expires to offline.
        """
        presence = UserPresence(
            user_id=user_id,
            state=PresenceState.ONLINE,
            last_seen=datetime.utcnow()
        )
        
        # Store with TTL (auto-expires to offline)
        await self.redis.set(
            f"presence:{user_id}",
            presence.to_json(),
            ex=int(self.online_ttl.total_seconds())
        )
        
        # Notify subscribers
        await self._notify_subscribers(user_id, presence)
    
    async def set_typing(self, user_id: str, chat_id: str):
        """
        Mark user as typing in a specific chat.
        
        Rate limited: max 1 update per second per chat.
        """
        # Rate limit check
        rate_key = f"typing_rate:{user_id}:{chat_id}"
        if await self.redis.exists(rate_key):
            return  # Already sent recently, skip
        
        # Set rate limit marker (1 second)
        await self.redis.set(rate_key, "1", ex=1)
        
        # Update typing state
        await self.redis.set(
            f"typing:{user_id}:{chat_id}",
            "1",
            ex=int(self.typing_ttl.total_seconds())
        )
        
        # Notify only the other participant(s) of this chat
        await self._notify_chat_participants(
            chat_id, 
            user_id, 
            PresenceState.TYPING
        )
    
    async def get_presence(self, user_id: str) -> Optional[UserPresence]:
        """
        Get current presence for a user.
        
        Used when opening a chat to show current status.
        """
        data = await self.redis.get(f"presence:{user_id}")
        
        if data:
            return UserPresence.from_json(data)
        else:
            # No presence record = offline
            # Get last seen from persistent storage
            last_seen = await self._get_last_seen(user_id)
            return UserPresence(
                user_id=user_id,
                state=PresenceState.OFFLINE,
                last_seen=last_seen
            )
    
    async def _notify_subscribers(
        self,
        user_id: str,
        presence: UserPresence
    ):
        """
        Notify users who care about this user's presence.
        
        Optimization: Only notify users who have this user's
        chat currently open (not all contacts).
        """
        # Get list of users with this chat open
        subscribers = await self.redis.smembers(
            f"presence_subscribers:{user_id}"
        )
        
        for subscriber_id in subscribers:
            await self.notifier.send_presence_update(
                subscriber_id,
                presence
            )
    
    async def subscribe_to_presence(
        self,
        subscriber_id: str,
        target_user_id: str
    ):
        """
        Subscribe to presence updates for a user.
        
        Called when user opens a chat.
        """
        await self.redis.sadd(
            f"presence_subscribers:{target_user_id}",
            subscriber_id
        )
        
        # Auto-expire subscription after 10 minutes
        await self.redis.expire(
            f"presence_subscribers:{target_user_id}",
            600
        )
    
    async def unsubscribe_from_presence(
        self,
        subscriber_id: str,
        target_user_id: str
    ):
        """
        Unsubscribe from presence updates.
        
        Called when user closes a chat.
        """
        await self.redis.srem(
            f"presence_subscribers:{target_user_id}",
            subscriber_id
        )
Quick check3 questions · +5 XP each

Offline messages and presence

  1. Q1/3 Bob comes online and the server starts draining his offline queue. When should each message be deleted from the server?

    Answer & explanation

    Answer: B

    Writing to the socket does not prove arrival: if the connection drops mid-drain, deleting early would lose messages. The delivery ACK is the proof. The 30-day expiry is only a backstop for messages that are never delivered.

  2. Q2/3 Bob's phone dies suddenly, so it never sends an "offline" event. How does the presence system still show him offline?

    Answer & explanation

    Answer: D

    Presence is stored with a TTL, so missing heartbeats make it expire to offline automatically, with no explicit event needed. Relying on the client to announce "offline" fails exactly in the crash and network-loss cases.

  3. Q3/3 Alice types 30 characters in about 3 seconds in one chat. Roughly how many typing updates does the presence manager send?

    Answer & explanation

    Answer: C

    set_typing is rate-limited to one update per second per chat, so a 3-second burst yields about 3 updates. Per-keystroke updates are exactly the chatter that makes presence ~10x message traffic.

Phase 5 · Stage 5 of 7 · 2 min

Phase 5: Scaling and Edge Cases#

Transcript
Interviewer

"WhatsApp grew from 0 to 500 million users in 5 years. How did they scale without rewriting everything?"

You

"Erlang's design made horizontal scaling almost automatic. But they still faced challenges..."

WhatsApp's Scaling Journey#

WhatsApp scaling timeline
  • 2009 —
    Launch
    • Few servers · Basic XMPP protocol · Thousands of users
  • 2011 —
    1 billion messages/day (October)
    • Custom Erlang backend · Modified ejabberd
  • 2012-2013 —
    200 million monthly users (2013)
    • 2 million connections on one server (January 2012) · Custom BEAM patches
  • 2014 (acquisition) —
    450 million monthly users (500 million by April)
    • ~50 billion messages/day (in + out) · ~550 servers · 32 engineers
  • 2020 —
    2 billion users
    • 100+ billion messages/day · Moved off IBM SoftLayer into Facebook's data centers (from 2017) · Still running on Erlang

Edge Cases#

Edge cases
Edge case 1: Celebrity with 10M Followers Posts Status
  • Note: a design exercise. Real WhatsApp Status goes to your contacts, end-to-end encrypted with a sender key (WhatsApp security whitepaper), so this models a public-follower feed.
  • Problem: Status update needs to reach 10 million people
  • Impact: Massive fan-out, could overwhelm system
  • Solution:
    • Status updates are pull-based, not push
    • User's device polls for updates when opening status tab
    • Updates cached at edge servers
    • Same update served to millions from cache
    • No per-follower push notification
Edge case 2: Group with 1000 Members
  • Problem: Each message needs 1000 deliveries
  • Impact: 1 message = 1000x amplification
  • Solution:
    • Sender Key protocol (not pairwise encryption)
    • Message encrypted once, decrypted by all members
    • Group metadata (membership) changes trigger rekey
    • Stagger delivery to avoid thundering herd
    • Members who leave can't read future messages
Edge case 3: Network Partition
  • Problem: User connected to Server A, recipient on Server B. Network between A and B fails
  • Solution:
    • Messages queued at Server A
    • Periodic retry to deliver to Server B
    • If partition heals, messages delivered
    • If partition persists, messages expire after 30 days
    • Sender simply never sees the double tick (no explicit failure notice)
Edge case 4: Device Goes Underwater (Literally)
  • Problem: User's phone destroyed, never comes back online
  • Impact: Their offline queue grows forever
  • Solution:
    • Messages expire after 30 days
    • Expired messages deleted from server
    • Sender never gets "delivered" status
    • No infinite queue growth
    • New device = new session, old messages lost
Quick check3 questions · +5 XP each

Scaling and edge cases

  1. Q1/3 A celebrity with 10 million followers posts a status. Which design avoids a massive fan-out?

    Answer & explanation

    Answer: A

    Pull plus edge caching serves one cached object to millions of readers, with no per-follower push. Staggering a push still does 10 million deliveries; it only spreads out the overload.

  2. Q2/3 In a 1,000-member group, what does the Sender Key scheme change compared with pairwise encryption?

    Answer & explanation

    Answer: C

    With a sender key shared over pairwise sessions, one ciphertext works for every member; the server still delivers it to each of them. A server that decrypts and re-encrypts would break end-to-end encryption, which is non-negotiable.

  3. Q3/3 A user's phone is destroyed and never reconnects. What stops their offline queue from growing forever?

    Answer & explanation

    Answer: D

    The 30-day TTL bounds queue growth; the sender simply never sees the double tick. The per-recipient queue does have a size cap, but it is thousands of messages, not one per sender.

Phase 6 · Stage 6 of 7 · 1 min

Phase 6: Why 50 Engineers?#

By 2015 WhatsApp served 900 million users with about 50 engineers (Wired, September 2015).

You

"The small team is the most impressive part. Here's why it worked..."

Why 50 engineers could build WhatsApp
1. Technology choices reduced complexity

Erlang's actor model:

  • No mutex/lock management (huge complexity source)
  • Built-in distribution (no Kafka/RabbitMQ to manage)
  • Hot code reloading (no complex deployment pipelines)
  • Supervision trees (automatic failure recovery)

FreeBSD:

  • Simpler than Linux (one distribution)
  • Better network stack for their use case
  • Team had deep expertise (ex-Yahoo)
2. Focus reduced scope

What WhatsApp DIDN'T build:

  • Advertising system (no ads = no ad tech team)
  • Social feed (no feed ranking team)
  • Recommendation engine (no ML team)
  • Third-party integrations (no platform team)
  • Multiple products (no product sprawl)
  • Enterprise features (initially)

Every feature NOT built = engineers NOT needed

3. Communication overhead is quadratic
  • Team of 50: 50 × 49 / 2 = 1,225 communication paths
  • Team of 500: 500 × 499 / 2 = 124,750 paths

Larger teams spend more time coordinating than coding. Small teams move FASTER for well-scoped problems.

4. Deep expertise > broad knowledge

The aim (as commonly described, not documented in detail) was that every engineer knew:

  • The entire codebase
  • Erlang deeply (not just superficially)
  • FreeBSD kernel internals
  • The complete message flow

No "that's not my service" mentality. Anyone could debug any issue.

5. Code review by founders

In early days, the founders were reportedly closely involved in reviewing backend code. This ensured:

  • Consistent style
  • No unnecessary complexity
  • Deep understanding by leadership
  • Quality over velocity
Quick check3 questions · +5 XP each

Why a small team works

  1. Q1/3 Communication paths in a team grow as n × (n − 1) / 2. How many paths does a team of 10 have?

    Answer & explanation

    Answer: C

    10 × 9 / 2 = 45. A team of 50 has 1,225 and a team of 500 has 124,750: coordination cost grows with the square of headcount, while the work done grows at best linearly.

  2. Q2/3 Which piece of infrastructure did Erlang's built-in distribution let WhatsApp avoid running?

    Answer & explanation

    Answer: B

    Erlang processes on different machines exchange messages with the same syntax as local ones, so routing between servers needed no extra broker cluster to deploy, tune and staff.

  3. Q3/3 What does Erlang's "let it crash" approach mean in practice?

    Answer & explanation

    Answer: C

    Instead of defensive code for every possible error, each connection runs in its own lightweight process under a supervisor. One crash affects one user, who reconnects, and everyone else carries on.

Final challenge · Stage 7 of 7 · 2 min

Final Challenge: Add Linked Devices#

The interviewer smiles. "One more requirement, and it's a hard one."

Interviewer

"Users want the same account on their phone and up to three other devices at once, each working even when the phone is switched off. End-to-end encryption stays non-negotiable. What changes?"

The linked-devices brief
  • One account, up to four devices, all able to send and receive on their own
  • The phone may be offline while the others are in use
  • The server still must not be able to read any message
  • A device that is offline for days must catch up when it returns
Your turn · checkpoint 5not answered

Who encrypts for whom now?

With one phone per account, each message was encrypted for one recipient device. Walk through what happens when Alice (two devices) messages Bob (four devices). Who encrypts what, what does the server track, and how does a new device get its history?

Think it through, or jot it down. Then compare.

One strong answer

One sound design (close to what end-to-end messengers use):

  • A key per device: every device has its own identity key and its own sessions. An account is a list of devices.
  • The sender fans out: Alice's phone encrypts the message separately for each of Bob's four devices and for her own other device, so all of them show the conversation. The server can't do this fan-out: it can't read the message, so it can't re-encrypt it.
  • The server tracks devices, not just users: a device list per account, and one delivery queue per device. Each device acknowledges and deletes from its own queue, with the same 30-day expiry.
  • History to a new device: sent from an existing device of the same account, encrypted end to end; the server only relays it.
  • Trust changes are visible: adding a device changes the account's key list, which the other side can be shown.
  • The cost: N devices means N encryptions and N deliveries per message, which is fine for chats and is why large groups use sender keys instead.
Quick check4 questions · +5 XP each

Final challenge · Quest check

  1. Q1/4 Alice has two devices and Bob has four. Alice sends Bob one message from her phone. With per-device encryption, how many encrypted copies does her phone produce?

    Answer & explanation

    Answer: C

    One for each of Bob's four devices, plus one for Alice's own second device so her conversation stays in sync. The sending phone itself already has the plaintext.

  2. Q2/4 Why can't the server just copy one encrypted message to all of Bob's devices?

    Answer & explanation

    Answer: B

    A ciphertext made for one device's session can only be opened by that device. Only a party holding the plaintext, the sender, can encrypt it for the others.

  3. Q3/4 One of Bob's devices is offline when the message arrives. What happens to its copy?

    Answer & explanation

    Answer: C

    Each device is just another recipient with its own offline queue, under the same deliver-then-delete and 30-day expiry rules as before.

  4. Q4/4 In a 1,000-member group using sender keys, how many times is each message's content encrypted when it is sent?

    Answer & explanation

    Answer: A

    Sender keys are distributed to members pairwise ahead of time, so each message is encrypted once and every member can decrypt it. Membership changes trigger a rekey so people who leave can't read what follows.

Wrap-up · 6 min

Interview Conclusion#

Transcript
Interviewer

"Excellent coverage. Quick questions:"

Interviewer

"If you were starting today, would you still use Erlang?"

You

"It depends. Erlang is still excellent for this use case, but:

Advantages of Erlang:

  • Perfect for massive concurrency
  • Battle-tested at WhatsApp scale
  • 'Let it crash' philosophy reduces code complexity

Disadvantages:

  • Smaller talent pool (hard to hire)
  • Less library ecosystem than Go/Rust
  • Learning curve for new engineers

Modern alternatives:

  • Go: Good concurrency, larger talent pool
  • Rust: Performance, growing ecosystem
  • Elixir: Erlang VM with nicer syntax

For a startup, I'd probably choose Go for the talent pool. For WhatsApp's specific requirements, Erlang is still hard to beat."

Interviewer

"What's the hardest part of building a messaging system?"

You

"Three things:

  1. Exactly-once delivery semantics — Messages should arrive once, in order, or clearly fail. Network unreliability makes this hard.

  2. End-to-end encryption at scale — Key management, device verification, group re-keying. Most teams underestimate this complexity.

  3. Presence at scale — The 'chattiest' part of the system. Easy to ignore, hard to scale. Presence storms are a classic source of overload."

Summary: Concepts Applied from 10-Week Course#

Concepts from 10-week course in WhatsApp design
  • Week 1: Data at scale
    • Partitioning — Users partitioned across "islands"
    • Replication — Each island has a replica
    • In-memory storage — ETS/Mnesia for speed
  • Week 2: Failure-first design
    • Let it crash — Erlang supervision trees
    • Process isolation — One crash doesn't affect others
    • Circuit breakers — Island isolation
    • Retry — Message redelivery for transient failures
  • Week 3: Messaging & async
    • Offline queues — Messages stored until delivered
    • At-least-once delivery — With deduplication at client
    • Backpressure — Queue size limits
  • Week 4: Caching
    • Presence caching — In-memory with TTL
    • Write-back cache — For offline message persistence
    • Edge caching — Media and status updates
  • Week 5: Consistency
    • Eventual consistency — Presence can be stale
    • At-least-once — Messages, with client dedup
    • Message ordering — Per-conversation, not global
  • Week 9: Security
    • End-to-end encryption — Signal Protocol
    • Forward secrecy — Double Ratchet algorithm
    • Key management — X3DH key agreement
    • Server blind to content — it can't read messages (it still sees metadata)
  • Week 10: Operations
    • Hot code reloading — Deploy without restart
    • Observability — Per-process stats in BEAM
    • Small team — Simplicity enables velocity

Why WhatsApp Matters#

Why WhatsApp is an engineering marvel
Efficiency
  • 2M+ connections on a single server (vs ~100K typical)
  • 50 billion messages/day with 32 engineers
  • $19 billion acquisition with 55 employees
Reliability
  • 99.99% uptime target (this design's)
  • Messages reliably delivered worldwide
  • Works on 2G networks in developing countries
Privacy
  • End-to-end encryption by default
  • Even WhatsApp can't read your messages
  • Signal Protocol: Gold standard for secure messaging
Simplicity
  • No ads for its first 16 years (they arrived in the Updates tab in June 2025), no social feed
  • One feature done exceptionally well
  • Focus enabled small team to move fast

"The best systems are simple enough that a small team can understand them completely, yet powerful enough to serve billions of users."

WhatsApp proved that architecture beats headcount.

Self-Assessment Checklist#

After studying this case study, you should be able to:

Architecture:

Architecture0/3 ready
  • Explain why Erlang is well-suited for messaging
  • Design a connection handling system for millions of users
  • Implement offline message queuing and delivery

Distributed Systems:

Distributed Systems0/3 ready
  • Apply the "let it crash" philosophy
  • Design supervision trees for failure recovery
  • Handle network partitions gracefully

Security:

Security0/3 ready
  • Explain Signal Protocol at a high level
  • Understand forward secrecy and its importance
  • Design key exchange for end-to-end encryption

Performance:

Performance0/3 ready
  • Optimize presence/typing for high frequency updates
  • Use TTL-based expiry for ephemeral data
  • Apply batching and rate limiting

Team Efficiency:

Team Efficiency0/3 ready
  • Understand how technology choices affect team size
  • Value simplicity over cleverness
  • Focus on doing one thing well

Sources#

Architecture and Scale:

Erlang and FreeBSD:

End-to-End Encryption:

System Design Resources:

Further Reading#

Official Documentation:

Conference Talks (Highly Recommended):

  • Talk

    Rick Reed - "Scaling to Millions of Simultaneous Connections" (Erlang Factory 2012)

    • YouTube: Search "Rick Reed WhatsApp Erlang"
  • Read

    Rick Reed - "That's 'Billion' with a 'B': Scaling to the Next Level at WhatsApp" (Erlang Factory 2014)

    • Details on scaling to hundreds of millions of users (11,000+ cores, 70M+ Erlang messages/second)
  • Blog

    Anton Lavrik - "A Reflection on Building the WhatsApp Server" (Code BEAM 2018)

    • Post-Facebook acquisition insights

Engineering Blogs:

Erlang/BEAM Resources:

Cryptography and Security:

Books:

  • Book

    "Designing Data-Intensive Applications" by Martin Kleppmann

    • Chapters on messaging and distributed systems
  • Book

    "Programming Erlang" by Joe Armstrong (Erlang creator)

    • Understanding the language that powers WhatsApp
  • Book

    "Designing Elixir Systems with OTP" by James Edward Gray II

    • Modern take on BEAM VM patterns
  • Book

    "System Design Interview Vol 1 & 2" by Alex Xu

    • Messaging system design patterns

Related Systems to Study:

  • Read

    Telegram: Different architecture (MTProto protocol)

  • Docs

    Signal: Open-source reference implementation

  • Read

    Discord: Modern take on real-time messaging

  • Read

    Slack: Enterprise messaging architecture

Research Papers:

  • Paper

    "The Signal Protocol" - Formal security analysis papers

  • Read

    "Asynchronous Ratcheting Trees" - Group messaging improvements

  • Read

    "SoK: Secure Messaging" - Survey of secure messaging systems

Podcasts and Videos:

  • Blog

    Software Engineering Daily: Episodes on messaging systems

  • Talk

    InfoQ: Conference talks on distributed systems

  • Talk

    Strange Loop Conference: Talks on Erlang/BEAM

"A few dozen engineers. Hundreds of millions of users. Tens of billions of messages a day. The right architecture makes the impossible possible."

this lesson in shorts · 3

play them all →

One idea from this lesson in about a minute each, made for your phone.

End of Side quest 02 · WhatsApp Messaging
Clear the questFinish WhatsApp.Stages clear as you read them. Marking complete unlocks after ~6 more min of reading.
  • 0/7stages cleared

Follow-up drills

0 of 10 practised

You presented WhatsApp Messaging. Now the interviewer pushes back. Answer each follow-up out loud or in the box, then reveal a strong answer.

Follow-up 1 / 10 · scale & numbers

“How many servers do you need just to hold the connections, and why does that number shape the whole design?”

Pushes on · sizing a chat backend by messages instead of connections

Revealing marks it practised.
design sheet · 7 diagrams · pdfRevise the whole design in ten minutesThe numbers, architecture, sequence, class and state diagrams, decisions and failure modes of this quest on a few printable pages.open the sheet →

Go deeper · reading trail

all reading →

Real write-ups from the teams that built these systems. Each opens on the publisher's site in a new tab.

WhatsAppBlog post · ~2 min

1 million is so 2011

2012
What
A short 2012 WhatsApp post showing one server holding over 2 million TCP connections.
Key idea
A single FreeBSD box running Erlang held 2.27 million open sockets with CPU to spare, the efficiency behind WhatsApp's tiny server fleet.
Why here
The 'Why Erlang?' deep dive of this quest in one screenshot: lightweight processes per connection and a tuned kernel.
taught here: Why Erlang

Our link checker received HTTP 400 (bot filtering); the page was opened and read with a second fetcher on 2026-10-18.

MetaBlog post · ~8 min

How WhatsApp enables multi-device capability

2021
What
How WhatsApp let up to four companion devices work without the phone, keeping end-to-end encryption.
Key idea
Each device has its own identity keys, and senders encrypt every message separately for each of the recipient's devices (client fan-out).
Why here
The Signal-protocol deep dive of this quest extended to many devices per user, and why the server never sees plaintext.
taught here: End-to-end encryption
MetaBlog post · ~11 min

Deploying key transparency at WhatsApp

Sean Lawlor, Kevin Lewi · 2023
What
WhatsApp's rollout of automatic checks that the public keys it hands out are genuine.
Key idea
Public keys are kept in an Auditable Key Directory whose changes can be verified with proofs, so a swapped key would be detectable.
Why here
Closes the gap in this quest's encryption design: end-to-end encryption is only as strong as the key directory.
taught here: Key distribution trust

New lessons by email

New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.

Discussion · …

Public: everyone reading this lesson sees comments. Be kind and keep it on the material.

Sign in to ask a question or answer one. Reading stays open to everyone.
loading the discussion…