System Design Interview Crash Course
Vote

0% completed

​

Design WhatsApp

1. Problem Definition and Scope

2. Clarify functional requirements

3. Clarify non-functional requirements

4. Back of the envelope estimates

5. API design

6. High level architecture

7. Data model

8. Core flows end to end

Flow 1: User A sends a message to User B (Both Online)

Flow 2: User A sends a message to User B (User B is Offline)

Flow 3: User B comes Online (Synchronization)

9. Caching and read performance

10. Storage, indexing and media

11. Scaling strategies

12. Reliability, failure handling and backpressure

13. Security, privacy and abuse

14. Bottlenecks and next steps

The complete picture

Summary

The decisions that mattered

The numbers that forced them

What breaks first

If the interviewer pushes

Image

This lesson designs WhatsApp, a messaging app where people send text and photos to one another and expect them to arrive at once. The hard part is not storing the messages. It is finding, for every single message, the one server that holds the recipient's open connection, and getting the message there within a fraction of a second.

1. Problem Definition and Scope

We are designing a global, real-time messaging app. Users send text and media to other users, and the messages should arrive within a fraction of a second when both people are online.

The system must deliver every message reliably, with low delay, for about a billion people a day. A message that the app shows as sent must never disappear.

  • Main User Groups: People using the mobile app on iOS and Android.

  • Main Actions: Sending text messages, sharing images, and seeing whether a message was delivered and read.

  • Scope:

    • Included: One-to-one text chat, basic group chat, message receipts (sent, delivered, read), offline support, and online status.
    • Excluded: Voice and video calls (VoIP, voice over the internet), Status and Stories, details of the web client, and the deep cryptographic details of end-to-end encryption (E2EE).

Agreeing on scope first matters because calls and Stories are each a separate design. Leaving them out lets us spend the time on the part that makes WhatsApp hard: routing each message to the right connection, quickly and without losing it.

Image

2. Clarify functional requirements

Functional requirements list what the system must do for its users.

Must Have:

  • 1-on-1 Messaging: Users can send and receive text messages in real time.
  • Group Messaging: A user can send one message to a group of up to 256 members, and every member receives it.
  • Message Receipts: Small marks next to each message show how far it has traveled.
    • Sent: The message reached our server and is stored (one gray tick). Before that, the app shows a small clock.
    • Delivered: The message reached the recipient's phone (two gray ticks).
    • Read: The recipient opened the chat and saw it (two blue ticks).
  • Offline Support: If the recipient is offline, the server keeps the message and delivers it as soon as the recipient reconnects.
  • Media Sharing: Users can upload and send images and videos.

Nice to Have:

  • Last Seen: Users can see when a contact was last online.
  • Push Notifications: A notification from the phone's operating system wakes the device when a new message arrives.

Notice that receipts turn every message into several small events. One message produces the message itself, a "delivered" receipt and a "read" receipt, and all three travel through the same servers.

Image

3. Clarify non-functional requirements

Non-functional requirements describe how well the system must work: how big, how fast, and how reliable.

  • Target Users: 1 billion daily active users (DAU), meaning 1 billion different people use the app on a normal day.
  • Message Volume: 50 billion messages per day, about 50 messages per user.
  • Latency: Latency is the time a user waits. When both users are online, a message should reach the recipient's phone in under 200 ms.
  • Availability: 99.99%. People depend on messaging every day. For reference, 99.99% still allows about 52 minutes of downtime a year, so the design must contain failures rather than assume none.
  • Ordering: Within one chat, every device must show the messages in the same order. Messages from one sender must appear in the order that sender wrote them.
  • Durability: Once the sender sees one tick, the message must never be lost, even if a server crashes a moment later.
  • Data Retention: The server keeps every message permanently, in encrypted form. Because the server cannot read the messages, this stored history is useful only to a device that holds the right keys. A brand new phone cannot read it on its own, so moving history to a new phone is a separate backup feature. Keeping history is a choice made for this design. The real WhatsApp deletes a message from its servers once it is delivered, and keeps an undelivered message for a limited time (about 30 days).

The 200 ms target and the durability rule pull in opposite directions. Durability needs a safe write to disk before we confirm anything, and every step we add before delivery uses part of the 200 ms.

Image

4. Back of the envelope estimates

These estimates are rough on purpose. Their job is to show which part of the system needs the most attention.

  • Traffic (messages per second):
    • Total messages: 50 billion per day.
    • Seconds in a day: about 86,400.
    • Average rate: 50,000,000,000 ÷ 86,400 ≈ 580,000 messages per second.
    • Peak rate, for example at midnight on New Year's Eve: about 3 times the average ≈ 1.7 million messages per second.
    • Database operations: each message is saved in two tables, the Messages table and the recipient's unread queue, and the queue entry is deleted when the phone confirms delivery. That is about 3 database operations per message, or roughly 5 million per second at peak, before counting receipt updates.
    • Receipts: each message also creates a "delivered" and a "read" receipt. The servers that hold connections therefore handle about 3 times as many small events, roughly 5 million per second at peak.
  • Storage (Text):
    • Average message size: about 100 bytes.
    • Daily text storage: 50 billion × 100 bytes = 5 TB per day.
    • Yearly text storage: 5 TB × 365 ≈ 1.8 PB (petabytes). The database keeps 3 copies of each message for safety, so the disks hold about 5.5 PB a year.
    • Conclusion: No single machine can hold this. We need a database that spreads data across many machines (sharding).
  • Storage (Media):
    • Assume 10% of messages carry media, with an average size of about 200 KB.
    • Daily media storage: 5 billion × 200 KB = 1 PB per day.
    • Conclusion: Media must go to object storage (such as Amazon S3), not into the message database.
  • Bandwidth:
    • Media uploaded (ingress): 1 PB per day ÷ 86,400 seconds ≈ 11.6 GB per second.
    • Media downloaded (egress) is at least as large, because every file is downloaded by each recipient, and a photo sent to a group is downloaded many times.
    • Conclusion: Media downloads should come from a Content Delivery Network (CDN), which is a large set of cache servers placed close to users in many cities.

The text numbers look small next to the media numbers. The real pressure on the text path is not size but rate: 1.7 million messages a second, each one needing a lookup and a delivery within 200 ms.

Image

5. API design

A normal HTTP request works one way: the app asks, and the server answers. In chat, the server must be able to send a message to the app at any moment, and asking the server again and again (polling) would waste traffic and add delay.

We therefore use WebSockets. A WebSocket is a connection that starts as an HTTP request and then stays open, so both sides can send data at any time.

1. Connect (WebSocket Handshake)

  • GET /ws/connect
  • Headers: Authorization: Bearer <token>
  • Response: 101 Switching Protocols. The connection is now open and stays open.

2. Send Message (WebSocket Event)

  • Client sends:
{ "type": "send_message", "data": { "client_msg_id": "c-7f3a-0001", "to_user_id": "user-uuid-2", "content": "encrypted-blob", "type": "TEXT" } }
  • Note: The app creates client_msg_id before sending. If the app must send the same message again, for example after a dropped connection, the copy carries the same ID, so the receiving app can recognize and discard it. The content is already encrypted on the sender's phone, and the server cannot read it.

3. Upload Media (REST Endpoint)

  • POST /v1/media
  • Request: Multipart file (an image or a video), already encrypted on the sender's phone.
  • Response:
{ "media_id": "media-uuid-99", "url": "https://media-cdn.example.com/media-uuid-99" }

4. Acknowledge Message (WebSocket Event)

  • Client sends:
{ "type": "ack", "data": { "message_id": "msg-123", "status": "DELIVERED" } }
  • Note: The recipient's app sends this acknowledgment (ACK) when the message reaches the phone, and sends it again with READ when the user opens the chat. These two events are what turn the sender's ticks gray and then blue.

6. High level architecture

We separate the servers that hold the open connections from the servers that run the message logic. The connection holders must stay up for hours, while the logic servers can be added, removed and restarted at any time.

Client → Load Balancer → Gateway Service → Message Service → Database

  1. Client: The mobile app. It keeps its chat history in a local SQLite database on the phone, and it holds one open WebSocket connection to our servers.
  2. Load Balancer: It spreads millions of WebSocket connections across the gateway servers, sending each new connection to a gateway with few connections.
  3. Gateway Service (Connection Holder):
    • A stateful server, meaning it remembers something between requests: the open connections of the users attached to it.
    • It keeps a map of User ID → open connection, for the users connected to this one machine.
    • It works like a pipe: it passes messages from users to the Message Service, and it pushes messages down to users. It contains no message logic.
  4. Session Cache (Redis): Redis is an in-memory key-value store, which answers in well under a millisecond. Here it holds one shared map that answers the question "Which gateway server is User B connected to?".
  5. Message Service: A stateless service, meaning any of its machines can handle any request. It checks each message, stores it in the database, and routes it to the right gateway.
  6. Group Service: It stores who belongs to each group, and it sends one group message to every member (fan-out).
  7. Database (Cassandra): Cassandra is a wide-column database that spreads data across many machines and accepts a very high rate of writes. It stores chat history and the queue of messages each user has not yet received.
  8. Object Storage (S3): It stores encrypted images and videos.

Could a message broker sit between the gateway and the Message Service? Yes, it could. A message broker, such as Kafka, is a server that stores messages in an ordered log until another service reads them. The gateway would write each message to the broker, and the Message Service would read from it.

A broker in that position would give us four things:

  • Buffering: A sudden burst of messages waits in the broker instead of overloading the Message Service.
  • Decoupling: The gateway does not need to know which Message Service machines are alive. It only writes to the broker.
  • Durability: Once the broker has written a message to disk on several machines, a crash of a Message Service machine cannot lose it.
  • Ordering per conversation: If the broker uses the chat_id as its partition key, all messages of one chat go into one ordered log and are read in the order they were written.

It also has a cost. Every message makes one extra network hop and one extra disk write before any work on it begins, and readers often take messages in small batches. Inside one data center, this usually adds a few milliseconds, and more under heavy load. It also adds one more large system to run. And the sender's one tick must now mean "written to the broker", or the app would have to wait for both writes.

This design calls the Message Service directly for one-to-one messages, as a synchronous request (the gateway sends the request and waits for the answer). The direct path already has most of what the broker would add. Durability comes from writing to Cassandra before any delivery attempt. Ordering comes from the message_id that the Message Service assigns (section 7). For buffering, the Message Service is stateless, so we add machines when traffic grows, and section 12 explains how the gateways slow down a sender who sends too much.

We do use a broker where the work is slow or bursty: group fan-out, in section 11. There, one message becomes up to 256 deliveries, so the sender should not wait for all of them. The extra milliseconds are acceptable there, and that is why group messages arrive slightly later than direct ones.

Image

7. Data model

We need a database built for a very high rate of writes: about 580,000 messages a second on average, plus their receipts. A single SQL database cannot accept that many writes, and splitting one across many machines by hand is complex. We use Cassandra, which spreads rows across machines by a partition key, the column that decides which machine holds a row.

1. Messages Table (Chat History)

  • Partition Key: chat_id (the ID of a one-to-one chat or a group) together with a time bucket, such as the month. All recent messages of one chat sit together on the same machines, so loading a chat is one fast read. The bucket stops a chat that is years old from growing into one huge partition, which Cassandra handles badly.
  • Clustering Key: message_id, a TimeUUID, which is a unique ID that also contains a timestamp. The clustering key sorts the rows inside a partition, so the messages are stored in time order.
ColumnTypeDescription
chat_idUUIDPartition key.
bucketINTPartition key. The month the message was sent.
message_idTIMEUUIDClustering key. Set by the Message Service.
client_msg_idTEXTSet by the sender's app, used to discard duplicates.
sender_idUUIDWho sent it.
contentBLOBEncrypted payload the server cannot read.
statusINTSent, Delivered, Read.
  • Ordering: The Message Service creates the message_id when it stores the message, and every device shows a chat in message_id order. Because the server sets the ID and not a phone, a phone with a wrong clock cannot change the order.
  • One sender can send two messages so quickly that two different Message Service machines store them at almost the same moment. To keep that sender's order exact, the app also numbers its own messages in each chat, inside the encrypted content. The receiving app never shows a sender's message 5 before that sender's message 4.
  • The status column fits one-to-one chats. In a group, every member has their own status, so a real system keeps group receipts in a small separate table keyed by message and member.

2. User_Unread_Queue (Offline Optimization)

  • Partition Key: user_id.
  • Clustering Key: message_id.
  • Other columns: chat_id, so the full message can be found in the Messages table.
  • Purpose: This table lists, for each user, the messages that have not yet reached their phone. The Message Service adds an entry when it stores a message, and deletes it when the recipient's phone sends a DELIVERED acknowledgment. When a user comes online, one read of this table shows exactly what they missed, instead of a scan of every chat they belong to. This table costs one extra write for every message, even when the recipient is online. We accept that cost because an entry kept until the phone confirms is what guarantees that a message lost in transit is sent again.

3. Group_Members (SQL or NoSQL)

  • group_id (PK) and user_id (PK). It answers the question "Who should receive this group message?".

8. Core flows end to end

This section follows a message through the system in the three situations that matter: both users online, the recipient offline, and the recipient coming back online. For each step, we describe what happens, why the step is needed, and what it produces.

Flow 1: User A sends a message to User B (Both Online)

This is the normal case: both users have the app open and are connected to the internet.

Step 1: Connections are already open

What happens:

  • Both users opened a WebSocket connection to the gateway service when they started the app.
  • User A is connected to Gateway Node 1, and User B is connected to Gateway Node 2.
  • Redis (Session Cache) stores the mapping: User A → Gateway 1 and User B → Gateway 2.

Why: Opening a new connection takes several network round trips. If the connection is already open, a message can leave the phone at once.

Outcome: The system knows where each user can be reached.

Step 2: User A sends the message

What happens:

  • User A types "Hello" and presses send.
  • The app encrypts the text on the phone, creates a client_msg_id, and sends the message over its open WebSocket to Gateway Node 1.
  • The app shows the message at once with a small clock, which means "not yet confirmed by the server".

Why: Showing the message immediately makes the app feel fast. The clock tells the user honestly that the server has not confirmed it yet.

Outcome: The encrypted message has reached our first server.

Step 3: The Message Service stores the message

What happens:

  • Gateway Node 1 forwards the message to the Message Service with a direct request. It does not read or change the message.
  • The Message Service assigns a message_id. It writes the message to the Messages table with status SENT, and it adds an entry for User B to User_Unread_Queue. Both writes go in one logged batch, a Cassandra feature that makes sure that if one write is applied, the other is too.
  • The write counts as done when a majority of the copies, 2 out of 3, have it on disk.

Why: We save the message before we try to deliver it. If a server crashed after accepting the message but before saving it, a message the sender believes was sent would be lost. The queue entry means that if delivery fails for any reason, the message is still waiting for User B. We call the Message Service directly, not through a broker, because this write already makes the message durable, and a broker would add an extra hop and a few milliseconds to every message (section 6).

Outcome: The message is safely stored, and it will reach User B even if every later step fails.

Step 4: The sender gets one tick

What happens:

  • After Cassandra confirms the write, the Message Service tells Gateway Node 1 to send an ACK to User A, with the new message_id.
  • On User A's screen, the clock changes to one gray tick.

Why: The tick is a promise that the message is stored. That is why it waits for the write and does not appear earlier.

Outcome: User A knows the server has the message.

Step 5: Routing and delivery

What happens:

  • The Message Service asks Redis: "Where is User B connected?" Redis replies: "Gateway Node 2."
  • The Message Service sends the message to Gateway Node 2, which pushes it down the open WebSocket to User B's phone.
  • If Gateway Node 2 replies that User B is no longer connected, because the Redis entry was out of date, the Message Service treats User B as offline and continues with Flow 2.

Why: Out of thousands of gateways, only one holds User B's connection. Redis answers that question in under a millisecond, which keeps us inside the 200 ms target.

Outcome: The message is on User B's phone. The app decrypts it and shows it.

Step 6: Receipts travel back

What happens:

  • User B's app sends ACK: DELIVERED to Gateway Node 2, which forwards it to the Message Service.
  • The Message Service deletes User B's entry from User_Unread_Queue and updates the status in the Messages table.
  • It looks up User A in Redis, and Gateway Node 1 pushes the receipt to User A. The tick becomes two gray ticks.
  • When User B opens the chat, the app sends ACK: READ along the same path, and the ticks turn blue.
  • If User A is offline by then, the receipt waits for User A in the same way a message would.

Why: Only User B's phone can say that the message really arrived. Deleting the queue entry at this moment, and not earlier, is what guarantees that an undelivered message is never forgotten.

Outcome: Both users see the same status, and the server no longer has any work left for this message.

Image

Flow 2: User A sends a message to User B (User B is Offline)

This flow handles a recipient whose phone has no internet connection or whose app is closed.

Step 1: Send and store (same as Flow 1)

What happens:

  • User A sends the message, and the Message Service stores it in the Messages table with status SENT.
  • In the same batch, it adds an entry for User B to User_Unread_Queue, a table built for fast lookup by user.
  • User A receives one gray tick.

Why: The sender's part does not depend on the recipient. The message is stored before anyone knows whether User B is online.

Outcome: The message is safe, and it is already listed as waiting for User B.

Step 2: The lookup finds no connection

What happens:

  • The Message Service asks Redis where User B is connected.
  • Redis returns nothing, because User B's entry expired when the connection closed.

Why: An empty answer is the signal that a push over WebSocket is impossible right now.

Outcome: The Message Service knows User B is offline.

Step 3: The message waits in the queue

What happens:

  • The entry written in step 1 stays in User_Unread_Queue. It holds User B's ID, the chat_id and the message_id.
  • No extra write is needed, and the sender keeps one gray tick until User B's phone confirms delivery.

Why: The queue is where the system remembers that it still owes User B this message. It is keyed by user, so finding everything User B missed later takes one read.

Outcome: The message will be delivered the next time User B connects.

Step 4: A push notification wakes the phone

What happens:

  • Because there is no open connection, the Message Service calls the Push Notification Service.
  • That service sends a notification through Apple's service (APNs) or Google's service (FCM, Firebase Cloud Messaging), which can reach a phone even when our app is closed.
  • The notification says only that a new message is waiting, for example "New message from User A". It does not contain the message text, because the server cannot read it.

Why: Our servers cannot start a connection to a closed app. Only the phone's operating system can wake the app, and APNs and FCM are the way to ask it to.

Outcome: User B sees a notification. When they open the app, Flow 3 begins.

Image

Flow 3: User B comes Online (Synchronization)

This flow explains how User B catches up on missed messages when they open the app.

Step 1: Reconnection

What happens:

  • User B opens the app. The load balancer sends the new WebSocket connection to any gateway with room, for example Gateway Node 3.
  • Gateway Node 3 writes the new mapping to Redis: User B → Gateway 3.
  • It tells the Message Service that User B is now connected.

Why: From this moment, new messages for User B must go to Gateway 3, so the map must change before anything else.

Outcome: User B is reachable again, through a new gateway.

Step 2: Checking the queue

What happens:

  • The Message Service reads User B's partition of User_Unread_Queue in Cassandra.
  • The gateway does not do this itself, because it contains no message logic.

Why: One read of a table keyed by user returns everything User B missed, across all of their chats.

Outcome: The Message Service has the list of waiting message IDs, for example 5 of them.

Step 3: Bulk delivery

What happens:

  • For each entry, the Message Service reads the full message from the Messages table, using its chat_id and message_id.
  • It sends all 5 messages down the WebSocket to User B in one batch, in message_id order. A user who was away for weeks receives the backlog in pages, not all at once.
  • User B's app sends ACK: DELIVERED for each message. A new message that arrives during the sync may reach the phone twice, once live and once from the queue, and the app discards the copy by its client_msg_id.

Why: Sending the backlog in one batch is much faster than one message at a time, and sending it in order keeps the chat readable.

Outcome: User B's phone has every missed message, in the right order.

Step 4: Cleanup and receipts

What happens:

  • For each DELIVERED acknowledgment, the Message Service deletes that entry from User_Unread_Queue.
  • It also sends a delivered receipt to each original sender, who now sees two gray ticks.

Why: An entry is deleted only after the phone confirms it, so a message lost in transit is sent again at the next connection. This is called at-least-once delivery: every message arrives, and a duplicate is possible but is discarded by the app.

Outcome: The queue for User B is empty, and every sender knows their messages arrived.

Image

9. Caching and read performance

  • Session Cache (Redis):
    • Data: Key: User ID. Value: the ID of the gateway that holds the user's connection.
    • Why: It is read for every message delivered, about 1.7 million times a second at peak. It must answer in a few milliseconds at most, and Redis usually answers in under one.
    • TTL: Each entry has a time to live (TTL), after which it expires. The gateway deletes the entry when a connection closes normally. Each heartbeat, a small "I am still here" message the app sends every few seconds, renews the TTL. If a gateway crashes and cannot delete its entries, they expire on their own.
  • User Profile Cache:
    • Data: Display name, profile picture URL and status text.
    • Why: Profile data rarely changes but is read very often.
  • Chat History:
    • We do not cache chat history in Redis. It is far too large, petabytes in total.
    • The app keeps recent history in its local SQLite database, so opening a chat usually needs no server read at all. When the server must return messages, for example during a sync, Cassandra reads them in order from one partition, which is fast.

10. Storage, indexing and media

  • Database (Cassandra):
    • It holds all text messages, receipts and the unread queues.
    • Indexing: The primary key (chat_id and bucket, then message_id) supports the query "get the messages of chat X after time Y" without any extra index.
  • Media Storage (S3 and CDN):
    • We never store image or video files in Cassandra.
    • Flow:
      1. The sender's app encrypts the image on the phone with a new random key.
      2. It uploads the encrypted file to the media endpoint, which stores it in S3.
      3. The server returns a media ID and a download URL.
      4. The app sends a normal encrypted message that contains the URL and the key.
      5. The recipient's app reads the URL, downloads the file from a CDN (such as CloudFront) for a fast download anywhere in the world, and decrypts it with the key.
    • Because the key travels only inside the encrypted message, S3 and the CDN store files that our servers cannot open.

11. Scaling strategies

  • Gateway Scaling:
    • Assume one gateway holds about 100,000 connections. If a large share of 1 billion users is connected at the same time, we need thousands of gateways.
    • The load balancer sends each new connection to a gateway with few connections.
    • If a gateway crashes, its 100,000 users lose their connections and reconnect to other gateways. Each app waits a random extra time before it reconnects (jitter), so they do not all arrive in the same second.
  • Database Partitioning:
    • Cassandra places each partition on machines chosen by a hash of its partition key (chat_id and bucket).
    • With billions of chats, this spreads the data and the writes evenly across the cluster.
  • Group Chat Fan-out:
    • Fan-out means turning one message into one delivery for each recipient. For a group of 200 people, the server must produce 200 deliveries.
    • The Message Service stores the group message once and then publishes one "group message" event to Kafka. The sender gets their tick without waiting for the 200 deliveries.
    • Background workers read the event, look up the members in Group_Members, and deliver to each member: a push through their gateway if they are online, a queue entry and a notification if they are not.
    • The Kafka partition key is the group_id, so all events of one group stay in one ordered log, and the workers handle them in order.
    • With the Signal protocol, the sender's app encrypts a group message once with a group key that every member already holds. The server copies one encrypted blob to each member, and it still cannot read it.

12. Reliability, failure handling and backpressure

  • No Single Point of Failure:
    • Every layer runs on many machines spread across several availability zones (AZs), which are separate data centers in one region. Cassandra keeps 3 copies of each row, in different zones.
    • Gateways are not copies of one another. A gateway crash does not lose any message, because every message was stored before delivery, but it does drop that gateway's connections until the apps reconnect.
  • Store and Forward:
    • We guarantee at-least-once delivery. A message stays in the recipient's unread queue until their phone confirms it. If we are not sure that User B got it, for example after a timeout during the ACK, we send it again.
    • The app discards duplicates using the client_msg_id, which is the same on every copy.
  • Backpressure:
    • Backpressure means slowing down the intake of work when the parts that process it are full.
    • A sender sending too fast: The sender's gateway limits how many messages one connection may send per second. Above the limit, it stops reading from that socket for a moment, and TCP flow control makes the sender's phone wait.
    • A recipient reading too slowly: If a phone on a weak network cannot receive as fast as messages arrive, its gateway stops pushing to it. The messages are not lost, because they wait in the unread queue and are sent when the phone catches up or reconnects.

13. Security, privacy and abuse

  • End-to-End Encryption (E2EE):
    • WhatsApp uses the Signal protocol, a widely trusted, published design for end-to-end encryption.
    • Each phone creates its own key pairs. The private keys never leave the phone. The server stores only public keys, and it hands them out so that two phones can set up an encrypted session.
    • The server processes encrypted blobs. It cannot read the messages or the media.
    • The server can still see metadata, meaning information about the messages: who sent a message to whom, when, and how large it was. It needs this metadata to route, order, store and track receipts.
  • Authentication:
    • Users sign up with a phone number, confirmed by a one-time code sent by SMS.
    • Each WebSocket connection is authenticated with a short-lived session token.
  • Abuse:
    • Rate limiting on sending, so that one account or bot cannot flood other users.
    • Users can block other users. The server knows the block list and drops messages from a blocked user before delivery, without reading them.

14. Bottlenecks and next steps

  • Bottleneck: Group Fan-out:
    • For very large groups, for example 10,000 members, pushing every message to every member is too slow and too expensive.
    • Next Step: Switch to a "pull" model for large groups. The message is stored once, and members fetch the latest messages when they open the group, instead of the server pushing every message to everyone.
  • Bottleneck: Global Latency:
    • A user in Australia sending a message to a user in the UK may wait longer if both connect to a data center in the US.
    • Next Step: Run gateways in many regions close to users, and connect the regions with a fast private network.

The complete picture

Every diagram above shows one part of the system. This one shows all of them together: the direct path for messages between two online users, the offline path through the unread queue, the group path through Kafka, and the media path through S3 and the CDN. Notice that every path writes to Cassandra before it delivers anything, and that the Redis session map is shared by the direct path and the group path.

The complete architecture, with caches, queues and scaling strategies marked
The complete architecture, with caches, queues and scaling strategies marked

Summary

Say this first. The hard problem in WhatsApp is not storage. It is routing. Each user holds an open connection to one server in a fleet of thousands, and for every message the system must answer "which server is User B on?" in under a millisecond, after it has stored the message safely. That store-then-route sequence is the center of the design.

The decisions that mattered

DecisionWhyWhat it costs
Stateful gateways separated from the stateless Message ServiceConnections must live somewhere, but the message logic should scale and restart on its ownA gateway crash drops every connection on it, and those apps must reconnect
Gateway calls the Message Service directly, with no broker in betweenThe Cassandra write already makes the message durable, and a broker adds a hop and a few milliseconds to every messageNo broker to absorb a burst, so the Message Service must scale out and senders are rate limited
Redis holds the user-to-gateway mapIt is read for every message and must answer in under a few millisecondsIf Redis is lost, the gateways must write their entries again before delivery works normally
Store before attempting deliveryA crash between accepting and storing would lose a message the sender saw as sentOne batch write to two tables on the critical path
A queue of undelivered messages per user, written for every messageScanning every chat to find missed messages is far too slow, and an entry kept until the phone confirms makes delivery at-least-onceOne extra write and one delete per message, and the same message is referenced in two tables
Group fan-out through KafkaUp to 256 deliveries must not block the senderGroup messages arrive slightly later than direct ones

The numbers that forced them

NumberWhat it decided
1.7 million messages a second at peakNothing slow can sit on the send path
100 bytes per messageText is small. 1.8 PB a year, 5.5 PB with three copies, is a sharding problem, not a hard one
1 PB a day of mediaMedia goes to object storage, and the message carries only a URL and a key
Under 200 milliseconds deliveryOne durable batch write, one cache lookup and one push, with no queue on the direct path

What breaks first

  1. Very large groups. Fan-out by looping over members stops working somewhere past a few hundred members. Above that, members should pull messages when they open the group instead of the server pushing to everyone.
  2. A gateway crash. A hundred thousand apps reconnect at once. Without random jitter on the retry, they arrive in the same second and overload the gateways they land on.
  3. Latency between continents. Two users on opposite sides of the world may route through one distant region. Regional gateways on a private network are the fix.

If the interviewer pushes

  • "Why not just poll?" At a billion users, polling creates load in proportion to the number of users, not the number of messages, and it still adds delay. An open connection uses some memory but almost no CPU while it is idle.
  • "How do you avoid duplicate messages on retry?" The app creates the message ID before sending. We guarantee at-least-once delivery, and the receiving app discards any copy with an ID it has already seen.
  • "Why not put Kafka between the gateway and the Message Service?" It would add buffering, decoupling, durability and per-chat ordering, but also an extra hop and a few milliseconds on every message. The direct path already gets durability from the Cassandra write and ordering from the server-assigned ID, so we keep the broker for group fan-out, where the delay is acceptable.
  • "If it is end-to-end encrypted, what does the server actually do?" Routing, ordering, storage and delivery receipts. It moves an encrypted blob it cannot read.
  • "How does a new device get old messages?" Not from the server alone. The server's copy is encrypted with keys that stayed on the old phone, which is why restoring history from a backup is a separate feature.
Aleksei Bondarev

Aleksei Bondarev

· a month ago

If I understand correctry, basically, we can use here an any message broker between gateway and message service, but it brings bigger delay of delivery, right?

Show 1 reply

Reading Progress

0%


Vote for new content

On This Page

1. Problem Definition and Scope

2. Clarify functional requirements

3. Clarify non-functional requirements

4. Back of the envelope estimates

5. API design

6. High level architecture

7. Data model

8. Core flows end to end

Flow 1: User A sends a message to User B (Both Online)

Flow 2: User A sends a message to User B (User B is Offline)

Flow 3: User B comes Online (Synchronization)

9. Caching and read performance

10. Storage, indexing and media

11. Scaling strategies

12. Reliability, failure handling and backpressure

13. Security, privacy and abuse

14. Bottlenecks and next steps

The complete picture

Summary

The decisions that mattered

The numbers that forced them

What breaks first

If the interviewer pushes