MediumProblem 10 of 36 in the system design series
Design WhatsApp: Real-Time Chat, Offline Delivery and Group Messages

A messaging app like WhatsApp lets people send short texts to a friend or a group and expect them to land almost immediately, wherever the recipient happens to be. Behind the familiar tick marks sits a system that maintains an open line to an enormous number of devices simultaneously and moves messages between them in well under a second.
This question appears in interviews because it tests a different muscle from most CRUD designs. The hard parts are stateful: long-lived connections that must be tracked and routed to, phones that drop offline mid-conversation, and a promise that a message, once acknowledged, will never vanish. Group chats then multiply every delivery by the number of members.
A convincing answer for a WhatsApp-like service explains how a message finds the server holding the recipient's connection, what exactly has happened by the time each tick appears, and how ordering and deduplication survive flaky mobile networks, retries and reconnects. It should also show where the design deliberately keeps state small.
This is an interview practice design for a WhatsApp-like service, built from first principles. It is not a description of how WhatsApp is actually built.
Functional Requirements
The core feature is one-to-one messaging: a user sends text to another user and it arrives in real time. Group chats extend that to conversations of up to roughly a thousand members, which is large enough to stress delivery but small enough to keep fan-out bounded.
Two requirements make the system more than a relay. Messages to an offline person must be held and handed over when that person reconnects, a store-and-forward model. And the sender sees per-message status through sent, delivered and read indicators, which means the backend must track acknowledgements, not just push bytes. Voice and video calling, status stories and payments are excluded since each is a large product in itself.
One-to-one chat
Send a message to another user and they get it instantly.
real-time
Group chats
Chat with a group of up to about a thousand members.
≤ 1,024 members
Offline delivery
Messages wait for offline users and arrive when they reconnect.
store & forward
Delivery status
Sent, delivered and read ticks for every message.
✓ · ✓✓ · read
- Out of scope today: voice and video calls, status stories, payments
Non-Functional Requirements
When both participants are online, a message should arrive in under 500 ms; anything slower makes a conversation feel laggy. Delivery is guaranteed: once the sender's phone shows a single tick, the message must survive server crashes and a recipient who stays offline for days. In practice that means at-least-once delivery combined with deduplication.
Order within a chat must be preserved, so an answer never shows up above the question it responds to. And all of this has to hold at enormous scale, with something like 500 million devices holding open connections simultaneously. That last requirement shapes the architecture more than any other.
Low latency
Online users receive messages almost instantly.
< 500 ms
Guaranteed delivery
A sent message is never lost, even if a phone is offline.
at-least-once
Ordering
Messages in a chat appear in the order they were sent.
per-chat order
Massive scale
Billions of users, hundreds of millions connected at once.
500M connections
Scale Estimation
Assume around 1 billion daily active users, each sending roughly 40 messages a day. That is about 40 billion messages daily, or approximately 460K per second on average, with peaks close to 1 million per second.
Connections matter even more than message rate. At peak, perhaps 500 million phones are online at once. If a single well-tuned server holds about 500K mostly idle sockets, we need on the order of 1,000 chat servers just to keep those connections alive. Storage is comparatively modest: at about 100 bytes per text, the daily volume is near 4 TB, and messages only live on the server until they are delivered. The conclusion is that the connection layer, and fast routing between connections, is the center of the design.
1billion
Daily active users
≈ 40 messages each / day
460K avg
Messages per second
40B / 86,400 · peak ≈ 1M
500million
Open connections
≈ 1,000 servers × 500K
4TB
Message data per day
40B × 100 B, until delivered
Long-lived connections → the connection layer is the heart of the design
Core Entities & API
The entities are User, Chat (either one-to-one or a group), Message and Device. Device is listed separately because one person may have several, and each holds its own connection and its own delivery state.
Rather than a REST call per message, every device opens a single WebSocket at /connect and keeps it alive. Sending is a send_message event carrying the chat ID, the text and a client-generated message ID; the server answers with an ack containing the server message ID and a sequence number. Incoming messages arrive as server-pushed new_message events, and the device must reply with an ack of its own so the server knows delivery succeeded. Infrequent setup operations, like creating a group, still use plain HTTP such as POST /chats.
Core entities
UserChat (1:1 · group)MessageDevice
API
- WS /connect→ one socket per device, kept open
- WS send_message { chat_id, client_msg_id, text }→ ack { msg_id, seq }
- WS new_message (server push) { msg_id, chat_id, from, seq, text }→ client replies ack(msg_id)
- POST /chats { members[], name? }→ { chat_id }
High-Level Design
Each phone maintains one WebSocket to a chat server, which is what lets the backend push a message the instant it exists rather than waiting to be asked. Suppose Alice, connected to chat server A, messages Bob. Server A hands the message to a message service, which first writes it to a message store such as Cassandra, in Bob's inbox, and then acknowledges Alice. That durable write is what earns the first tick.
Next the message service consults a session registry, a Redis map from user ID to the chat server currently holding that user's socket. It learns that Bob is on server B, relays the payload to that machine, which writes it onto Bob's open socket. When Bob's phone acknowledges receipt, Alice's message gains its second tick and the inbox entry is removed. If the registry shows Bob is offline, the message simply stays in his inbox and a push notification nudges his phone to reconnect.
Bob offline? Message waits in his inbox + push notification
Deep Dive 1 — Connection Servers
Short polling has the phone ask every few seconds whether anything new has arrived. It works over plain HTTP, but almost every response is empty, and a message can sit undelivered for a full polling interval. Long polling improves latency by holding each request open until data appears, yet the client must re-establish the request after every delivery, and the channel only flows from server to client.
WebSockets keep one connection open in both directions with only a few bytes of framing per message, which suits chat perfectly. The cost is statefulness: a chat server now owns specific users, which complicates deploys and failover. Because most sockets are idle most of the time, an event-driven networking stack such as Netty lets one machine hold around half a million of them.
Phones send a small heartbeat every 30 seconds, and each registry entry carries a TTL, so a device that silently disappears ages out of the registry instead of attracting misrouted messages. When a chat server dies, its devices fail over by connecting to a healthy peer and then catch up on anything delivered while they were away. WebSockets win on latency and efficiency.
Short polling
- + Plain HTTP, simple
- − Mostly empty responses
- − Delay up to the interval
Long polling
- + Near real-time
- − Reconnect after every message
- − One-way, server to client
WebSocketsChosen
- + Instant, both directions
- + Tiny overhead per message
- − Stateful servers
- Each server holds ≈ 500K idle sockets · event-driven I/O
- Heartbeat every 30 s · registry entry with a TTL
- Server dies → phones reconnect elsewhere and sync missed messages
Deep Dive 2 — Reliable Delivery
Reliability starts on the phone. Every outgoing message gets a unique client-generated ID, so if the network drops and the app resends, the server recognizes the duplicate and discards it. The server then persists before it promises anything: the message is written to the recipient's inbox, and only after that write succeeds does the sender receive an ack. A single tick therefore means the message is safely stored, not merely in flight.
Delivery follows. An online recipient gets the message pushed over the socket; an offline one keeps it in the inbox and receives a push notification. When the recipient's device acknowledges, the sender sees a second tick and the inbox entry is deleted.
Reconnection closes the loop. A phone coming back online asks for every message after the last sequence number it has, so gaps are impossible. The server assigns that sequence number per chat at write time, which gives one authoritative order that every device sorts by. Retries prevent loss, and client IDs prevent duplicates from appearing in the conversation.
- 1Client message IDPhone creates a unique ID, so retries are deduplicated
- 2Persist firstSave to the receiver's inbox, then ack: one tick
- 3Deliver or waitOnline: push on the socket. Offline: push notification
- 4Device ackReceiver confirms → two ticks → delete from inbox
- 5Sync on reconnectPhone pulls everything after its last sequence number
- Ordering: the server stamps a per-chat sequence number on every message
Deep Dive 3 — Group Fan-out
A message to a 1,000-member group means up to 1,000 deliveries, so each step is made cheaper. Group membership is cached, which avoids a database read on every message. The session registry is then used to bucket members by the chat server they are connected to; if 200 members share one server, the message service makes one call to it instead of 200.
For larger groups, fan-out is handed to Kafka. The sender receives an ack as soon as the message is stored, and background workers fan it out to members over the following moments. Offline members receive an inbox entry, either a copy or a pointer to the stored message, which they collect on reconnect.
Finally, group size is capped at around a thousand members, which keeps the worst-case cost of a single message predictable. Audiences much larger than that call for a separate broadcast-style channel rather than a chat group.
Cache the member list
Group membership comes from a cache, not the database, on every message.
Batch members by chat server
Use the registry to group members: one call per server, not one per member.
Fan out through a queue
Large groups go through Kafka, so the sender is never blocked.
Offline members get inbox entries
Each one gets a copy or pointer to fetch on reconnect.
Cap the group size
A limit around a thousand members keeps fan-out bounded.
Trade-offs
Persistent sockets give near-zero delivery delay and minimal per-message overhead, yet servers that own live connections are harder to roll out, rebalance and recover. Deleting messages once delivered cuts storage and improves privacy, while leaving chat history to live on the device and its backups rather than on the server.
At-least-once delivery with deduplication guarantees nothing is lost, but every client has to discard repeats using the message ID. Fanning group messages out on write gives members fast reads, at a cost that rises linearly with group size, which is why the cap matters. Summed up: store a message before acknowledging it, push it over open sockets, and require an ack at every hop.
WebSockets vs polling
Instant and cheap per message, but stateful servers are harder to deploy.
Delete after delivery
Less storage and better privacy, but chat history lives on the phone and its backups.
At-least-once + dedupe
No lost messages, but clients must ignore duplicates by message ID.
Fan-out on write for groups
Fast reads for members, but cost grows with group size.
Persist first. Push over sockets. Ack everything.
Key takeaways
- Assume about 500 million concurrent sockets: the connection layer of roughly 1,000 servers drives the design.
- Route messages through a Redis session registry that maps each user to the chat server holding their socket.
- Persist to the recipient's inbox before acking the sender, so one tick always means safely stored.
- Use client message IDs for dedupe and per-chat sequence numbers for ordering and gap-free sync.
- Fan group messages out by chat server, push large groups through Kafka, and cap group size.
Interview questions
How does a chat app deliver messages to users who are offline?
The server stores the message in the recipient's inbox before acknowledging the sender, so it is durable. If the session registry shows no active connection, a push notification is sent to wake the device. When the phone reconnects, it requests every message after the last sequence number it received, downloads them, acknowledges them, and the server then deletes the delivered entries.
Why use WebSockets instead of polling for a messaging app?
Polling either wastes requests on empty responses or delays messages by the polling interval, and long polling needs a fresh request after every message. A WebSocket stays open in both directions, so the server can push instantly with tiny framing overhead. The trade-off is stateful servers, which needs a session registry, heartbeats and reconnect logic to manage.
How do you keep chat messages in the correct order?
The server assigns a sequence number per chat when it persists each message, giving a single authoritative order regardless of client clocks or network timing. Devices sort by that number, and on reconnect they request everything after the highest sequence they hold. That also exposes gaps, so a missing message is detected and fetched instead of silently skipped.
How do you prevent duplicate messages when a phone retries?
Each message carries a unique ID generated by the sending device. If the network drops before the ack arrives, the app resends with the same ID, and the server recognizes it and returns the original ack instead of storing a second copy. Receivers also deduplicate by message ID, so at-least-once delivery never shows the same text twice.
How does a chat server know which server a user is connected to?
A session registry, typically a Redis map from user ID to chat server, records where each device's socket lives. Entries are written on connect and carry a TTL refreshed by heartbeats, so stale entries expire if a phone disappears. The message service looks up the recipient there and forwards the message to that specific server for delivery.
Keep practising
- Learn the building blocks in the free system design course.
- Rehearse answers with system design interview questions.
- See which open jobs fit your skills: upload your resume.