This gives away every stage. For a clean attempt, practise it first, then come back.
The short version
- Keep a connection open to every phone so the server can push messages instantly. Polling is too slow and wasteful.
- Order messages with a sequence number per conversation, never by the phone’s clock.
- Save the message before showing “sent”, and let the phone create the message ID so retries can’t make duplicates.
- With many connection servers, a registry says which one holds each user.
- Offline phones catch up by asking for everything after the last sequence number they have.
The 45 minutes
Before we start
WhatsApp, Telegram, Slack: you type, you hit send, and the message is on your friend’s phone almost before you’ve put yours down. If they’re offline, it waits patiently and arrives the moment they’re back. You see one tick, then two, then blue.
Three things make this hard. Speed: messages must be pushed to phones instantly, which means keeping millions of connections open. Order: replies must never appear before the question they answer. Reliability: a message that shows “sent” must never be lost, and retries must never show it twice.
Same order as always: requirements, quick maths, data and API, a design that grows, then deep dives.
Minutes 0–5Requirements
Interviewer: Design a chat app like WhatsApp.
You: One-to-one chats only, or groups too? How big can groups get?
Interviewer: Both. Groups of up to 500 people.
You: How many users and messages?
Interviewer: 50 million people a day, about 40 messages each. Around 20 million are connected at the same time at peak.
You: What happens when the other person is offline?
Interviewer: The message waits and arrives when they’re back. They also get a push notification.
You: Do we need the sent, delivered and read ticks?
Interviewer: Yes, all three.
You: Do people use more than one device?
Interviewer: Yes: a phone, plus web or desktop. Each shows the full history.
You: Do we keep history forever, and images?
Interviewer: Text forever. Images yes. Voice and video calls are out of scope.
You: Online status and “last seen”?
Interviewer: Nice to have.
Functional requirements
- Send and receive one-to-one messages in real time.
- Group chats of up to 500 people.
- Messages to offline users wait, and arrive when they come back, with a push notification.
- Sent, delivered and read ticks.
- Full history on every device.
Nice to have: online status and last seen. Out of scope: voice and video calls.
Non-functional requirements
- Fast: when both people are online, a message arrives in under about 200 ms.
- Never lost: once the sender sees a tick, the message is safe.
- In order: messages in a conversation always appear in the same order for everyone.
- No duplicates, even when phones retry on a bad connection.
- Scale: about 20 million open connections and 70,000 messages a second at the busiest time.
Minutes 5–10Back-of-the-envelope estimates
You: 50 million people times 40 messages is 2 billion messages a day, about 23,000 a second, maybe 70,000 at the busiest time.
You: Storage: at about 200 bytes a message, that’s 400 GB a day, around 146 TB a year, and we keep it forever.
You: The unusual number is connections: 20 million phones each holding a connection open. If one server can hold around 500,000, we need about 40 connection servers, plus spares.
500,000 per server is a safe plan. WhatsApp famously got about 2 million connections onto one carefully tuned server, which lesson 7.1 tells the story of. Leaving headroom means a server failing or being updated doesn’t overload the others.
Minutes 10–15Core entities and the API
Core entities
- User and device: a person, and each phone or laptop they use.
- Conversation: a one-to-one chat or a group, with its members and its latest sequence number.
- Message: belongs to a conversation, has a sequence number within it, a sender, the text (or an image link), and an ID created by the sender’s phone.
- Receipt: “delivered” or “read” up to a certain message, per member.
The API
Most chat traffic travels over one open WebSocket connection per device, not separate HTTP requests:
ws.send(message)Sent over the open connection. The phone creates the message ID, so a retry can’t create a duplicate.
{ "client_msg_id": "9f2c…", "conversation_id": "c_77", "text": "Running late!" }{ "ack": "9f2c…", "seq": 1042, "status": "sent" }ws.receive()The server pushes new messages and ticks down the same connection.
{ "conversation_id": "c_77", "seq": 1043, "from": "meera", "text": "No problem" }/v1/conversations/c_77/messages?after_seq=1030Catch up after being offline: everything after the last message this device has.
- 200
{ "messages": [ { "seq": 1031, "text": "…" } ], "latest_seq": 1043 }
Minutes 15–25The design, built up
Stage 1Ask the server for new messages every few seconds
You: The simplest version: messages go into a database, and every phone asks “anything new for me?” every three seconds.
It works for a small app. But messages can be up to three seconds late, and almost every one of those requests finds nothing new, so the server spends most of its effort saying “no”. Lesson 7.1 compares the options:
Stage 2Open connections: the server pushes messages
You: Better: each phone opens one WebSocket connection and keeps it open. A WebSocket is a connection that stays open in both directions, so the server can send a message down it the moment it arrives.
You: When you send a message, a message service gives it the next sequence number for that conversation, saves it, tells your phone “sent”, and pushes it down the other person’s connection.
This is instant and efficient. But one server can only hold so many connections, and with 20 million phones we need about 40 servers. That creates a new problem.
Stage 3Many gateways: find the right one, route between them
You: Now you’re connected to gateway 4, and Meera is connected to gateway 17. How does your message reach her?
You: When a phone connects, its gateway records “Meera is on gateway 17” in a fast shared registry, like Redis. To deliver, the message service looks Meera up and sends the message through a routing layer to gateway 17, which pushes it down her connection.
You: The registry entry expires unless the gateway keeps renewing it, so if a gateway crashes, its users simply disappear from the registry and are treated as offline until they reconnect elsewhere.
Lesson 7.1 shows a message finding its way across gateways:
Stage 4Offline users, presence and images
You: Three more things. If Meera isn’t in the registry, she’s offline: her message is already saved, so we send a push notification. When her phone reconnects, it asks for everything after the last sequence number it has, one quick read.
You: Presence, the “online” and “last seen” labels, comes from heartbeats the phones send. We only send someone’s status to people who currently have that chat open, otherwise every phone coming online would notify hundreds of contacts.
You: Images are uploaded straight to object storage, and the message just carries a link.
Here’s how the ticks work, online and offline:
Checking the design against the requirements
| Requirement | Where the design handles it |
|---|---|
| One-to-one messages in real time | WebSocket gateways push messages instantly |
| Groups of up to 500 | Saved once, delivered to each online member, push to the rest |
| Offline delivery with a push | Saved first; push notification; catch-up by sequence number |
| Sent, delivered, read ticks | Small confirmation messages flowing back to the sender |
| History on every device | Each device asks for everything after its last sequence number |
| Fast | Open connections; no polling |
| Never lost | Saved before the “sent” tick |
| In order | Sequence number per conversation |
| No duplicates | Message ID created by the phone; repeats are ignored |
| 20M connections, 70K messages a second | ~40+ gateways, registry for routing, store split by conversation |
Deep diveReal-time delivery
Interviewer: Why WebSockets? Talk me through the options.
Bad solutionPolling
The phone asks “anything new?” every few seconds. Messages arrive late, and most requests are wasted. Polling more often makes it faster but even more wasteful.
Good solutionLong polling
The phone asks, and the server holds the request open until there’s something to send, then the phone immediately asks again. Much faster. But connections are constantly torn down and rebuilt, and sending from the phone needs separate requests.
Great solutionWebSockets
One connection stays open both ways. The server pushes messages, ticks and typing indicators the moment they happen, and the phone sends over the same connection. The trade-off is that servers must hold millions of open connections, and we need the registry to know who’s where.
Deep diveKeeping messages in order
Interviewer: How do you make sure everyone sees messages in the same order?
Bad solutionOrder by the phone’s clock
Phone clocks can be wrong by minutes. Order by them, and a reply can appear above the question it answers.
Good solutionOrder by the server’s clock
Better, but different servers’ clocks also drift a little, and two messages can arrive in the same millisecond.
Great solutionA sequence number per conversation
The message service gives each message in a conversation the next number: 1041, 1042, 1043. Everyone sorts by it, so everyone sees the same order. Each conversation has its own counter, so busy conversations never slow each other down, and we never need a single global order across the whole app. Lesson 7.1 shows what goes wrong otherwise:
Deep diveNever lost, never twice
Interviewer: A user on a train sends a message, the connection drops, and the phone retries. What happens?
You: Two rules handle this.
You: First, the message service saves the message before replying “sent”. If the server crashed before saving, the phone never got its tick, so retrying is safe and correct.
You: Second, the phone gives every message its own ID when it’s created. If a retry arrives with an ID we’ve already saved, we return the original result instead of saving it again. So retrying is always safe, and the message appears once.
You: The same idea protects the receiving side: a phone that’s sent a message with sequence number 1042 twice just ignores the second copy.
Deep diveGroup chats
Interviewer: How does a message reach a 500-person group?
You: Save it once, in the group’s conversation, with one sequence number. Then deliver a copy to each member who’s online through their gateway, and send push notifications to the rest. 500 deliveries is fine.
You: For huge groups or broadcast channels with tens of thousands of members, I wouldn’t push to everyone. Members fetch new messages when they open the channel, just like reading a feed.
You: And read receipts in big groups are usually summarised, like “read by 230”, because tracking 500 separate ticks for every message is a lot of traffic for little value.
Deep diveStoring billions of messages forever
Interviewer: Where do messages live?
You: In a database that spreads data across many machines, like Cassandra, with the conversation ID as the partition key. Then a whole conversation lives in one place, already sorted by sequence number, and “give me everything after 1030” is one quick read.
You: Very active conversations would grow huge partitions, so I’d add a time bucket to the key, like conversation plus month.
Discord stores its messages exactly this way, channel ID plus a time bucket. Lesson 3.1 tells that story:
The complete architecture
Here’s everything together: the full design from the last stage, with every part we added along the way. If you only remember one picture from this question, make it this one.
How one request travels through it:
- Each phone keeps one WebSocket connection open to a gateway server, and the gateway records ‘I hold this user’ in the connection registry.
- You send a message. Your phone gives it its own ID and sends it down the connection.
- The message service gives it the next sequence number for that conversation, saves it, and only then tells your phone it’s sent (one tick).
- It looks up which gateway holds the other person and sends the message there through the routing layer. That gateway pushes it to their phone.
- Their phone confirms it arrived (two ticks), and later that it was read (blue ticks). Those confirmations travel back the same way.
- If they’re offline, a push notification goes out instead. When they reconnect, their phone asks for everything after the last sequence number it has.
How this answer gets scored
Interviewers don’t tick boxes, but their notes on this question tend to land on the same few moments. Here’s what each one sounds like from the other side of the table.
| Moment | What earns credit | What costs you |
|---|---|---|
| The first five minutes | Asking about open connections, offline delivery and devices | Asking only about messages per second |
| Real time | WebSockets, and why polling falls short | Polling every few seconds |
| Many servers | A registry of who’s connected where | Assuming everyone’s on one server |
| Order | A sequence number per conversation | Sorting by the phone’s clock |
| Reliability | Save before “sent”, and a phone-made message ID | Acknowledging before saving |
| Storage | Partition by conversation plus time bucket | One big messages table |
What the left column has in common: every choice points back to a requirement or a number from earlier. If you can say where each box came from, the interviewer will notice.
Stay in the loop
Get new problems and walkthroughs by email
One short email when something new is published: a deep-dive walkthrough, an LLD problem, or a real outage retold. No spam, unsubscribe any time.