Why a chat system is a classic design question, and what it teaches
A chat system lets people send messages and receive them within a moment, on any device, even if one end was offline when the message was sent. It combines real-time delivery, durable storage, ordering, presence and fan-out to many devices.
It gives you two things interviewers always ask about:
- Low latency: a message should appear almost at once, so the server must push it rather than wait to be asked.
- Durability and availability: no message is lost when a server crashes or a user is offline, and chat keeps working when one server fails.
Requirements to confirm first:
- Functional: one-to-one and group chat, online and offline delivery, message history, delivery and read receipts, optionally media.
- Non-functional: low delivery delay, no lost messages, messages in order within a conversation, millions of simultaneous connections.
Skip the design, or simplify it, when:
- Messages can arrive minutes late: ordinary requests and a database are enough.
- Only a few users are online: one server and one database are enough.
Rough capacity estimation
These numbers are illustrative assumptions, not measurements of any real service. Change them for your own case and keep the method.
| Quantity | Assumption or calculation | Result |
|---|---|---|
| Daily active users | Assumed | 100 million |
| Messages per user per day | Assumed | 40 |
| Messages per day | 100 million x 40 | 4 billion |
| Average messages per second | 4 billion / 86,400 seconds | about 46,300 |
| Peak messages per second | Assumed 3 times the average | about 139,000 |
| Storage per day | 4 billion x 200 bytes (assumed size with metadata) | about 800 GB |
| Storage per year | 800 GB x 365 | about 292 TB, or about 876 TB with 3 copies |
| Users connected at once | Assumed 10 percent of 100 million | 10 million |
| Chat servers | 10 million / 100,000 connections per server (assumed) | about 100, plan for about 150 with spare capacity |
| Heartbeats | 10 million connections, one ping every 30 seconds | about 333,000 per second |
Two things the numbers teach. Connections, not message rate, decide how many chat servers you need. And the heartbeats that keep connections alive are about seven times the average message rate, so they must be cheap to answer.
Try it
Ana sends three messages to Ben. Change how Ben receives them, then break something.
Chat System
Step 1 of 14 · Press Space to play, arrow keys to stepInitial state. Ana will send three messages to Ben. Every message is saved to the database before anything else.
- tick:
- 0
- requests:
- 0
- delivered:
- 0
- avg delay (ticks):
- -
1 tick = one step of the simulation.
Things to try:
- Run WebSocket with no failure. Each message is pushed the moment it is saved, with no waiting for a poll and no empty requests.
- Switch to Polling. Messages wait for the next poll, and some polls return nothing.
- Choose Recipient is offline with each design. All three messages still arrive, because each one was saved before delivery was attempted.
- Choose Chat server crashes with each design. Messages 1 and 2 were saved before the crash. Message 4 is sent while the server is down, so it is not saved yet: Ana's app keeps it and retries after the server returns. Nothing is lost because the app treats a message as sent only once the server acknowledges it.
What to watch (average delay in ticks, from the counters, for this three-message run):
| Failure | WebSocket | Polling |
|---|---|---|
| None | 0.0 | 1.7 |
| Recipient is offline | 1.3 | 6.7 |
| Chat server crashes | 0.7 | 3.7 |
The requests counter counts every push as well as every poll, so WebSocket shows 3 and polling shows 4 with no failure. The real saving is that polling's last two polls (ticks 9 and 12) return nothing, and every polled message waits for the next poll.
Ways to deliver messages
| Approach | How it works | Good when | Weakness |
|---|---|---|---|
| Short polling | The client asks for new messages every few seconds | Simple, rare updates | Delay up to the interval, and many empty requests |
| Long polling | The server holds a request open until there is news | WebSockets are blocked | Every message costs a new request and a held connection |
| WebSocket | One two-way connection stays open and either side can send | Real-time chat | Millions of open connections to manage |
| Server-sent events | The server streams events one way | Notifications, live feeds | Cannot send from client over the same stream |
| Push notification | The phone operating system wakes the app | The user is offline or the app is closed | Not guaranteed or instant, and needs a provider |
One chat server vs many
- One server knows every connected user, so delivery is a memory lookup. It cannot hold millions of connections or survive its own failure.
- Many servers each hold part of the connections. To send to Ben, the sender's server must find which server Ben is connected to, through a shared session store or a pub/sub channel, and forward the message there.
High-level design
| Component | What it does | What it does not do |
|---|---|---|
| Client app | Opens the WebSocket, makes the message ID, retries until acknowledged, remembers the last message ID it has | Decide the order of messages |
| Load balancer | Spreads new connections over the chat servers, including reconnects | Route individual messages |
| Chat server | Holds open connections, saves each message, acknowledges it, pushes to its own connections and forwards to other servers | Keep the only copy of anything |
| Session store | Maps each user to the chat server holding their connection; can be rebuilt from reconnects | Hold messages |
| Pub/sub channel | Carries "deliver this to user X" between chat servers when you do not look up the server first | Remember a message nobody was listening for |
| Message store | Keeps every message in order per conversation and is the truth | Know who is online |
| Push service | Asks the phone's push provider to wake a closed app | Guarantee delivery |
| Presence service | Tracks heartbeats and last seen | Carry messages |
The request path is: Ana, load balancer, her chat server, message store, session store lookup, Ben's chat server, Ben. If Ben is offline, the path ends at the push service instead.
How message delivery works
- Ana's app sends the message over its WebSocket to her chat server, with a client-made ID.
- The server saves it first (database or log) and acknowledges to Ana.
- It looks up where Ben is connected. If he is online, it pushes the message to that server, which sends it to his device.
- If Ben is offline, the message stays stored and a push notification is sent. When he reconnects, his app asks for everything after the last message ID it has.
- Ben's app acknowledges receipt, which produces the "delivered" tick.
Because the stored copy is the truth and each message has an ID, a retry never creates a duplicate: the server ignores an ID it has seen. This holds when the check for a seen ID and the save happen as one atomic step, scoped to the sender and the client-made ID. Delivery on the wire is therefore at least once (a retry may arrive twice), and the ID check makes the visible result exactly once. That is what idempotent means: repeating the request has the same effect as doing it once.
Chat design vs the alternatives
| Concern | WebSocket chat service | Polling an API | Email-style store and forward |
|---|---|---|---|
| Delivery delay | Near zero | Up to the poll interval | Minutes |
| Requests when idle | No message requests (only small heartbeats) | Constant | None |
| Works when the user is offline | Yes, with storage and push | Yes | Yes |
| Connections to manage | Many | Few | None |
| Complexity | High | Low | Low |
| Best for | Real-time conversations | Prototypes | Asynchronous messages |
Build the WebSocket service when conversations are real time. Polling is enough for a prototype.
Avoiding a new single point of failure
- Chat servers: keep them stateless apart from open connections, so a client that loses its server reconnects to another one through the load balancer and syncs from its last message ID.
- Session store and pub/sub: replicate them, and rebuild the map from reconnects if it is lost.
- Message store: replicate it and partition by conversation, so one conversation's messages live together and stay in order.
You accept a short reconnect delay when a server fails and some complexity around ordering.
Bottlenecks and how to relieve them
- Open connections per server. Memory and file handles cap a server. The 100,000 per server in the estimate is an assumption to load test, not a limit. Use a least-connections rule on the load balancer, and when deploying, drain a server gradually so its clients reconnect in small groups.
- Reconnect storm. When a server holding 100,000 connections dies, all its clients reconnect at once. Back-off (wait longer after each failed attempt) plus jitter (a random extra wait) spreads them out.
- Hot partitions. Partitioning by conversation keeps order, but one very busy conversation lives on one partition. That is fine for ordinary chats. The real cost in a large group is fan-out, copying one message to many recipients, which is why very large groups pull for quiet members.
- Presence heartbeats. In the estimate they are about seven times the average message rate. Answer them in the chat server's memory and update the shared presence store only when status changes.
- Session store lookups. One lookup per message. Cache recent answers, replicate the store, and rebuild it from reconnects if it is lost.
The trade-off throughout: you pay in operational care (draining, back-off, load testing) to keep the low latency that open connections give.
Common mistakes
- Sending before saving. A crash loses the message. Save, then deliver.
- Ordering by arrival time. Different servers disagree. Use a per-conversation sequence number.
- No client-generated ID. Retries create duplicates.
- Treating connection count as the only limit. Fan-out in large groups needs its own plan.
- Forgetting presence cost. Broadcasting every status change to every contact does not scale.
- No sync on reconnect. The client must ask for messages after its last ID.
How to talk about a chat system in an interview
- Confirm one-to-one vs group, history, receipts and the scale of simultaneous users.
- Choose WebSockets for delivery and explain why polling does not fit.
- Walk through save first, then deliver, how you find Ben's server, and what happens when he is offline.
- Mention ordering per conversation, idempotent retries, push notifications, and the trade-off: holding many open connections buys low latency at the cost of operational complexity.