Design Problems · Chat System

Design a Chat System

Real-time messaging: WebSocket gateways, routing between servers, message storage and ordering, delivery receipts, presence and offline push.

  • 8 min read
  • 9 practice questions
  • 1 graded problem
On this page
  1. Description
  2. The first version, and why it breaks
  3. The numbers
  4. Persistent connections
  5. Routing a message
  6. Order and duplicates
  7. Storing messages
  8. Groups
  9. Presence
  10. Which piece does what
  11. Common mistakes
  12. Check yourself
  13. Key takeaways
  14. Quick revise
  15. Practice

Description

A chat app has to feel instant: you send "Lunch?", and it appears on your friend's phone before you've put yours down. It also has to keep every message in order on every device, never lose one it said was sent, and cope when phones drop off the network. Designing WhatsApp, Messenger or Slack is a classic interview question because those few requirements force a design quite unlike an ordinary web API: long-lived connections, servers that hold state, and per-conversation ordering.

In this lesson:

  1. Why polling fails, and the numbers to design for.
  2. Persistent connections (WebSockets), and why they make chat servers stateful.
  3. Routing a message to whichever server holds the recipient.
  4. Keeping messages in order and making retries safe.
  5. Storing history, read receipts, groups and presence.

The first version, and why it breaks

The first version of a messaging app usually polls: every two seconds, each app asks the server "anything new for me?" With 100 million people online, that's 50 million requests a second, and almost all of them come back empty. Worse, a message can still take up to two seconds to show up.

Chat feels instant because the server pushes a message the moment it exists. To push, the server needs an open connection to every online phone and laptop. That one requirement shapes the whole design.

The numbers

  • 100 million daily users, with about 20 million connected at the busiest moment.
  • 10 billion messages a day, about 115,000 a second, peaking near 300,000 a second.
  • Delivery to an online recipient in under 500 ms. A message the server has acknowledged is never lost. History shows up on every device.
  • One-to-one chats, and groups of up to 500 people.

Persistent connections

Each online app opens a WebSocket, a long-lived connection that both sides can send on, to a chat server. A chat server can hold on the order of 100,000 mostly quiet connections, so 20 million online users need a couple of hundred servers. The load balancer in front spreads connections across servers (least connections works well), rather than individual requests.

Here's the catch: these servers are stateful. Bob can only be reached through the one server that holds his connection. Any server that wants to deliver a message to Bob has to find that server first.

Routing a message

Alice is connected to chat server 3 and sends Bob a message:

Bob's phoneChat server 17Session registryMessage storeChat server 3Alice's phoneBob's phoneChat server 17Session registryMessage storeChat server 3Alice's phonesend "Lunch?" (message ID m-81)store it, conversation 42, sequence 1093sent (sequence 1093)where is Bob?chat server 17deliver to Bob"Lunch?"delivered
Bob's phoneChat server 17Session registryMessage storeChat server 3Alice's phoneBob's phoneChat server 17Session registryMessage storeChat server 3Alice's phonesend "Lunch?" (message ID m-81)store it, conversation 42, sequence 1093sent (sequence 1093)where is Bob?chat server 17deliver to Bob"Lunch?"delivered
  1. Server 3 receives the message on Alice's connection.
  2. It stores the message first, then tells Alice it's sent. Storing first is what makes "never lost" true.
  3. It asks the session registry where Bob is. The registry is a fast store, such as Redis, that maps each user to the server holding their connection. A server writes an entry when someone connects, and keeps it alive with heartbeats.
  4. If Bob is on server 17, server 3 forwards the message there (directly, or through a publish/subscribe channel per server), and server 17 pushes it down Bob's connection.
  5. If Bob isn't connected anywhere, the message goes to an offline queue. A worker sends him a push notification through Apple's or Google's push service (APNs or FCM). When Bob opens the app, it fetches everything after the last message it saw.

Order and duplicates

Two problems show up at scale.

Order. Messages from different people arrive at different servers, and phones' clocks don't agree. If you sorted by the sender's timestamp, two devices could show a conversation in different orders. The fix is a sequence number per conversation, handed out by the server when it stores the message. Every device shows the conversation in sequence order. You don't need one global counter for the whole system, which would be a bottleneck. Order only matters within a conversation.

Duplicates. A phone on a shaky train connection sends a message, doesn't hear back, and sends it again. The app gives every message a unique message ID, and the server checks it. If it's already stored, the server just says "sent" again instead of posting it twice.

Read more - why the app, not the server, picks the message ID

The ID has to exist before the first attempt leaves the phone, so the retry carries the same one. If the server assigned IDs, a retry would look like a brand-new message. A random UUID generated on the device works well.

Storing messages

Messages are written all the time and read one conversation at a time, newest first. A wide-column store (such as Cassandra) partitioned by conversation ID and sorted by sequence number fits that exactly: reading recent history is a single range scan of one partition. Old messages can move to cheaper storage.

Read receipts ("delivered", "read") are not flags on each message. In a 500-person group, that would be 500 flags per message. Instead, store one marker per person per conversation: "Bob has read up to sequence 1093."

Groups

A message to a 500-person group is stored once, under the group's conversation. Then it's delivered to each member: look up which servers members are connected to, send one batch to each of those servers, and queue push notifications for members who are offline. Huge broadcast channels, with tens of thousands of followers, work better with a pull model, just like celebrity posts in a news feed.

Presence

"Online" and "last seen" come from the registry's heartbeats. Telling every contact about every change is expensive: someone with 5,000 contacts whose phone keeps dropping signal on a train would cause a storm of updates. So presence updates are batched and slowed down, or only fetched when someone opens a conversation.

Which piece does what

NeedPiece
Instant delivery to online usersWebSocket connections to chat servers
Find the server holding a user's connectionSession registry (user → server, kept alive by heartbeats)
Same order on every deviceA sequence number per conversation, assigned when stored
Safe retriesA message ID from the app, checked by the server
History by conversationA store partitioned by conversation ID, sorted by sequence
Offline usersAn offline queue and push notifications

Common mistakes

  • Polling for new messages. Millions of empty requests a second, and still slow.
  • Acknowledging before storing. If the server crashes in between, an "ack'd" message is lost.
  • Ordering by the sender's clock. Devices disagree, so conversations look different on different phones.
  • A flag per message per reader for receipts. Groups make this explode. Store "read up to" markers.
  • Forgetting that chat servers are stateful. Without a registry, nobody knows where to deliver.

Check yourself

Try to answer before opening each one - recalling beats re-reading.

1. Why do chat servers need a session registry when ordinary web servers don't?

Show answer

A chat server holds live connections, so only the server with the recipient's connection can deliver to them. The registry maps users to servers, so any server can route a message to the right one. Ordinary web servers are stateless and can handle any request, so nothing needs to know where a user is.

2. Why a sequence number per conversation instead of the sender's timestamp?

Show answer

Device clocks disagree and can be wrong, so timestamps can put messages in different orders on different devices. A sequence number assigned by the server when it stores the message gives every device the same order.

3. An app resends a message after a timeout, but the first attempt actually worked. How does the design avoid a duplicate?

Show answer

The app sends the same message ID both times. The server sees it's already stored and simply acknowledges it again.

Key takeaways

  • Push over persistent connections (WebSockets). Polling at this scale means millions of empty requests.
  • Chat servers are stateful. A session registry maps each user to their server, so messages can be routed.
  • Store before acknowledging. Sequence numbers per conversation fix the order; message IDs make retries safe.
  • Partition messages by conversation. Read receipts are per-person "read up to" markers.
  • Store group messages once, deliver per server, and push to offline members through a queue.
  • Batch and slow down presence updates.

Quick revise

The whole skill on one page, for the night before an interview.

Everything from the lesson and the deep dive on one page. Skim it before a design interview, and open the full lesson for anything that doesn't ring a bell.

The numbers

QuantityEstimateSo...
Polling every 2 s with 100 M online50 M requests/s, nearly all emptyPush over persistent connections instead
Peak connections~20 M~200 chat servers at 100,000 connections each
Messages10 B/day ≈ 115,000/s, ~300,000/s peak
Requirements< 500 ms to online users; nothing acknowledged is lost; history on every deviceStore before acking

Which piece does what

NeedPiece
Instant deliveryWebSocket connections to chat servers (balance by connection, least-connections)
Find the recipient's serverSession registry: user → server(s), kept alive by heartbeats (Redis)
Same order on every deviceSequence number per conversation, assigned when stored
Safe retriesMessage ID generated on the device, checked by the server
HistoryStore partitioned by conversation ID, sorted by sequence (wide-column)
Offline usersOffline queue + push notification (APNs / FCM); app fetches "after sequence N"
Read receiptsOne "read up to sequence N" marker per person per conversation
Groups (≤ 500)Store once; deliver one batch per server; push to offline members
Huge channelsPull, like celebrity posts in a feed
PresenceHeartbeats; batch and slow down updates, or fetch on open

Sending a message

Alice's server stores it (gets sequence 1093) → acks Alice → asks the registry where Bob is → forwards to his server → pushes down his connection → Bob acks delivery. Bob offline → offline queue + push notification.

The traps

  • Polling for messages.
  • Acknowledging before storing - a crash loses an acked message.
  • Ordering by the sender's clock - devices disagree.
  • A delivered/read flag per message per reader - groups explode.
  • Forgetting chat servers are stateful - no registry, no routing.
  • Server-assigned message IDs - a retry looks like a new message.

Staff-level points, one line each

  • ~20 KB per idle connection: 100,000 connections ≈ 2 GB per server; 200 servers for 20 M (40 at 500,000 each with tuning). WhatsApp reported over 2 M connections per server with Erlang on FreeBSD.
  • Heartbeats every 30 s from 20 M clients ≈ 667,000 tiny messages/s - tune the interval; mobile apps lean on the OS push channel in the background.
  • A server with 100,000 connections dies: jittered reconnects over 30 s ≈ 3,333/s (all at once is a burst ~60× worse than a 60 s spread); registry entries expire by TTL; devices catch up "after sequence N". Deploys drain a few servers at a time.
  • Sequence numbers without a global bottleneck: one owner per conversation (partition leader or hashed server) incrementing in memory, handed over cleanly on failure; DB compare-and-set works but costs a round trip (Cassandra lightweight transactions are much slower).
  • Catch-up after a week: fetch "conversations changed since last sync" first, then each conversation lazily. Multiple devices = the registry maps a user to a set of servers.
  • End-to-end encryption (Signal protocol): no server-side search or content spam filtering; group sender keys rotated when members leave; new devices get history from another device or an encrypted backup.
  • Storage at 100 B/message: 1 TB/day, 3 TB with replicas, ~365 TB/year raw; media in object storage by reference.
  • One giant channel = one ever-growing partition → partition by (conversation, time bucket) (Discord: Cassandra by channel + bucket, later ScyllaDB). APNs payloads ≤ 4 KB, so notifications are previews or wake-ups.

Say it in an interview

  • "Clients hold a WebSocket; a session registry tells any server where a user is connected."
  • "The server stores the message and assigns the conversation's next sequence number before it acks the sender."
  • "Retries reuse the message ID from the device, so the server de-duplicates."
  • "If a chat server dies, clients reconnect with jitter and fetch everything after their last sequence number."

Practice

This skill comes with 9 multiple-choice questions that tell you which misconception a wrong answer reveals and 1 graded problem. Your answers are graded and your mastery is tracked, so you'll know when the skill holds up. Running code and saving progress need a free account.

Practice Design a Chat System free

Design a Chat System

HardProSystem design problem

Design the backend of a messaging app with one-to-one and group chats. Arrows go from the caller to what it calls; mark an arrow async when the caller hands work off.

The full statement, the examples and the hidden test suite are part of AI Proof Engineers Pro.

Practice Design a Chat System free

No card needed. Lessons, practice questions and easy problems are free forever.

Free plan, no card · 7-day Pro trial · Cancel anytime