Skip to content
SparkTime, home
System Design From Zero to Interview-Ready
Lessons
1 · System Design Foundations
2 · APIs and Communication
3 · Capacity Estimation
4 · Core Building Blocks
5 · Databases
6 · Distributed Systems
7 · Reliability and Operations
8 · Architecture and Security
9 · Interview Method and Foundation Case Studies
10 · Real-World Case Studies
11 · Large-Scale Case Studies
12 · Interview Readiness
Lesson 34 of 45

Lesson 34 · Real-World Case Studies

Design a Chat System Like a Messaging App

Design one-to-one and group chat for tens of millions of concurrent users: WebSocket connection gateways, a session registry for routing messages to the right server, per-conversation sequence numbers for ordering, acknowledgements and deduplication, offline delivery with push and sync, multi-device cursors, presence, partitioned message storage, failures and security.

On this page

Key takeaways

  • Chat needs long-lived connections, so connection gateways are stateful and a session registry maps each user's devices to the gateway holding their connection.
  • Give every message a per-conversation sequence number assigned in one place; it defines order, lets clients detect gaps and makes retries deduplicable.
  • Store a message durably before acknowledging it, deliver at-least-once, and let clients deduplicate by conversation and sequence.
  • Offline users get a push notification and fetch missed messages by syncing from their last seen sequence when they reconnect.
  • Partition message storage by conversation and time bucket; treat presence and typing indicators as cheap, lossy, eventually consistent signals.

A chat app feels simple: type, send, it appears on your friend's phone. Behind that, the system must keep tens of millions of connections open, find which of hundreds of servers holds the recipient's connection, keep messages in order, never lose one, never show one twice, deliver to people who are offline, and keep every device a person owns in sync. And it must do it in a fraction of a second.

Chat is a favourite interview question because it forces you to deal with stateful connections, ordering, delivery guarantees and partitioned storage all at once. This case study works through it with the course method.

What you will learn

#
  • How to scope a messaging product and estimate connections, messages and storage.
  • Why chat uses persistent connections, and how to scale stateful gateways.
  • How a session registry routes a message to the right server.
  • Per-conversation sequence numbers for ordering, acknowledgements and deduplication.
  • Offline delivery, push notifications and sync on reconnect, across several devices.
  • Group chat fan-out, presence and typing indicators.
  • How the design fails, and how it is secured and observed.

1. The problem

#

Build a messaging service with one-to-one chats and group chats (up to 500 members), delivered in real time to online users and reliably to offline users, with message history available on all of a user's devices. Users can send text and media (photos and short videos) and see delivery and read receipts.

2. Clarify the requirements

#

Question

Assumed answer

Impact

One-to-one and groups?

Both; groups up to 500 members

Fan-out per message

Very large broadcast channels?

Out of scope (different design)

Bounded fan-out

Multiple devices per user?

Yes, up to 5 (phones, web, desktop)

Per-device delivery state

History?

Stored on the server and synced to devices; kept indefinitely unless deleted

Large, growing storage

Media?

Photos and short videos

Object storage, not the message store

Receipts?

Delivered and read receipts

Extra messages, eventual

Presence?

Online and last seen, subject to privacy settings

Lossy, high-volume signal

End-to-end encryption?

Not in this design's scope; mentioned as an extension

Server can read content in this design

Latency?

When both users are online in one region, under 500 ms at p99

Real-time path

Functional requirements

  1. Send and receive messages in one-to-one and group conversations, in order.
  2. Deliver in real time to online devices; deliver later (with a push alert) to offline devices.
  3. Sync history to every device; fetch older messages by scrolling.
  4. Delivery and read receipts; presence and typing indicators.
  5. Send media (via object storage).

Out of scope: voice and video calls, broadcast channels with huge audiences, end-to-end encryption design, search across messages.

Non-functional requirements

Quality

Target

Latency

p99 under 500 ms sender-to-recipient when both online in one region

Durability

A message acknowledged to the sender is never lost

Ordering

Messages in a conversation appear in the same order on every device

No duplicates visible

Retries never show a message twice

Availability

99.99% for sending and receiving

Scale

Below

3. Assumptions and scale estimation

#

These figures continue the worked example from articles 06 and 07 (assumptions, not any real product's numbers):

Code
100 million DAU; 40 messages sent per user per dayMessages: 4 billion/day -> about 46,000/s average, about 92,000/s at peak (factor 2)Deliveries (fan-out): 80% one-to-one (1 recipient), 20% to groups (average 20 members, 19 others)  deliveries per message = 0.8 x 1 + 0.2 x 19 = 4.6; x devices online per recipient (say 1.3)  -> about 92,000 x 4.6 x 1.3 = about 550,000 device deliveries/s at peakConcurrent connections at peak: assume 30% of DAU online, 1.3 devices each  -> about 39 million concurrent connectionsStorage: average message about 200 bytes raw, about 400 bytes stored with overhead  -> 4 billion x 400 B = 1.6 TB/day, about 580 TB/year (before replication)Media: stored in object storage, referenced by key (not in the message store)

Conclusions:

  • Connections: about 39 million long-lived connections. If one gateway server holds about 100,000 connections comfortably (an assumption to confirm with load tests, since it depends on memory, TLS and traffic per connection), we need about 400 gateway servers, plus headroom and zone redundancy.
  • Writes: about 92,000 messages per second at peak into a store growing by more than half a petabyte a year: a partitioned, write-optimised store (article 17).
  • Fan-out: deliveries are about 6 times the message rate, so routing must be efficient.

4. API design

#

Real-time traffic flows over a WebSocket connection (article 05), which gives both sides a persistent, full-duplex channel (RFC 6455). Messages on it are small JSON (or binary) frames:

Code
Client -> server: SEND   { client_msg_id: "c-7f3a", conversation_id: "cv_88", body: "On my way", media_key: null }Server -> client: ACK    { client_msg_id: "c-7f3a", conversation_id: "cv_88", seq: 1042, server_ts: "..." }Server -> client: MSG    { conversation_id: "cv_88", seq: 1042, sender_id: 42, body: "On my way", server_ts: "..." }Client -> server: DELIVERED { conversation_id: "cv_88", up_to_seq: 1042 }Client -> server: READ      { conversation_id: "cv_88", up_to_seq: 1042 }Client -> server: TYPING    { conversation_id: "cv_88" }          (ephemeral, not stored)

Ordinary HTTP APIs handle everything that is not real-time:

Code
GET  /v1/conversations?cursor=...                         -> my conversations with last message and unread countGET  /v1/conversations/{id}/messages?before_seq=1000&limit=50   -> history, scrolling backGET  /v1/conversations/{id}/messages?after_seq=1042       -> sync after reconnectPOST /v1/conversations                                    -> create a groupPOST /v1/media/uploads                                    -> presigned upload URL for a photo or video

Two identifiers do a lot of work:

  • client_msg_id, generated by the sending device, makes SEND idempotent: if the device retries after a lost acknowledgement, the server recognises it and returns the same seq.
  • seq, assigned by the server per conversation, defines the order and lets devices detect gaps.

5. Data model

#
SQL
-- Wide-column style (article 17): partition by conversation and time bucket, cluster by sequence.CREATE TABLE messages (    conversation_id  text,    bucket           text,          -- for example '2026-10'; bounds partition size    seq              bigint,    client_msg_id    text,    sender_id        bigint,    body             text,    media_key        text,    server_ts        timestamp,    PRIMARY KEY ((conversation_id, bucket), seq)) WITH CLUSTERING ORDER BY (seq DESC);
Code
conversations         (conversation_id PK, type one_to_one | group, created_at, last_seq)conversation_members  (conversation_id, user_id, role, joined_at)       PK (conversation_id, user_id)user_conversations    (user_id, last_activity_at, conversation_id, unread_count)                      partition user_id, sorted by last activity -> "my chat list"device_cursors        (user_id, device_id, conversation_id, delivered_seq, read_seq)dedupe                (conversation_id, client_msg_id) -> seq            (short TTL, for retries)
  • The hottest access pattern, "latest 50 messages of a conversation", is one range read on one partition.
  • user_conversations is a denormalised read model (article 27) for the chat list, updated on each message.
  • device_cursors records how far each device has received and read, which powers sync and receipts.
  • Partition keys: conversation_id (plus bucket) for messages; user_id for chat lists and cursors.

6. Baseline architecture

#
Code
Phone A --WebSocket--> Chat server --> DatabasePhone B --WebSocket--/      (if B is connected to the same server, push MSG to B)

One server holds all connections, saves each message and forwards it to the recipient's connection. That works for thousands of users. It cannot hold 39 million connections, and with many servers, the recipient's connection is usually on a different server from the sender's.

7. What breaks first?

#
  1. Connections: tens of millions of long-lived connections need hundreds of stateful servers.
  2. Routing: a message arrives at gateway 17, but the recipient's devices are connected to gateways 203 and 331.
  3. Ordering: two group members send at the same moment through different servers; all devices must show the same order.
  4. Reliability: connections drop constantly on mobile networks; acknowledgements get lost; senders retry.
  5. Offline users: most recipients are not connected at any moment.
  6. Storage writes: about 92,000 messages per second at peak.

8. Scaling the design

#

Split connection handling from message processing

#
Code
                    +-------------------------------+Devices --WS-->     | Gateway servers (stateful)    |  hold connections, authenticate,                    | ~400+ across zones            |  forward frames; no business logic                    +-------------------------------+                         |   ^                 heartbeats: register device -> gateway                         v   |                         |                    +--------------------+      +-----------------------+                    | Message service    |----->| Session registry      |  user/device -> gateway                    | (stateless)        |      | (Redis, TTL heartbeats)|                    +--------------------+      +-----------------------+                         |            +------------+-------------+-------------------+            v                          v                   v     Message store             Delivery fan-out       Notification system     (partitioned by           (routes to gateways    (push for offline devices,      conversation)             via internal pub/sub)  article 33)
  • Gateways are deliberately thin: they terminate TLS and WebSockets, authenticate the device at connect time, keep connections alive with heartbeats, and pass frames to and from the message service. Keeping logic out of them means they rarely need redeploying, and each redeploy disconnects users.
  • The message service is stateless and does the real work.
  • The session registry maps each online device to its gateway. Gateways register devices on connect and refresh entries with a short TTL (say 60 seconds, renewed every 20), so entries for crashed gateways disappear on their own.

Sending a message, step by step

#
Code
1. A's device sends SEND{client_msg_id, conversation_id, body} to its gateway.2. Gateway forwards to the message service.3. Message service:   a. checks A is a member of the conversation (authorisation);   b. checks dedupe(conversation_id, client_msg_id): if present, re-sends the ACK with the stored seq and stops;   c. assigns seq = next sequence number for the conversation (see below);   d. writes the message to the message store (durable, replicated);   e. records dedupe(conversation_id, client_msg_id) -> seq;   f. sends ACK{client_msg_id, seq} back to A's device;   g. publishes "deliver seq to the conversation's members" for fan-out.4. Fan-out: for each member device, look up the registry:   - online: route MSG to that device's gateway;   - offline: request a push notification (article 33) and rely on sync later.5. Recipient devices reply DELIVERED / READ with up_to_seq; cursors are updated and receipts relayed.

The sender's device shows a "sent" tick only after the ACK, which means the message is durably stored. Until then it shows "sending" and retries with the same client_msg_id.

Assigning sequence numbers

#

The sequence must be assigned in one place per conversation, so concurrent senders get distinct, increasing numbers. Options:

  • An atomic counter per conversation in a fast store (for example an atomic increment in Redis, or UPDATE conversations SET last_seq = last_seq + 1 ... RETURNING last_seq). Simple, but the counter store must be durable enough that sequence numbers are never reused after a failover (a reused number would collide with a stored message, which the primary key would reject, prompting a retry with the next number).
  • Single-writer partitions: route all messages for a conversation to one owner (for example by hashing conversation_id to a partition of a log such as Kafka, which preserves order within a partition (Introduction to Kafka)), and let that owner assign the sequence. Ordering and assignment come from the same place.

Either way, ordering is per conversation, not global, which keeps it cheap and parallel (article 18: never use wall clocks from different servers to order messages).

Routing between gateways

#

The message service must reach the gateways holding recipients' connections:

  • Direct: look up the gateway in the registry and call it (an internal RPC). Simple and fast; the service needs addresses of all gateways.
  • Pub/sub: each gateway subscribes to a channel named after itself (or after the users it holds), and the service publishes deliveries to those channels. Redis Pub/Sub is one lightweight option (Redis Pub/Sub); note that its messages are fire-and-forget (not stored if nobody is listening), which is acceptable here because the durable copy is already in the message store and devices sync by sequence.

Losing a real-time delivery is not data loss: the device will notice a gap in sequence numbers, or fetch on reconnect.

Offline delivery and sync

#
  • If no device of a recipient is online, the fan-out asks the notification system to send a push ("Asha: On my way"), respecting preferences (article 33). Push providers deliver to phones even when the app is closed (Firebase Cloud Messaging).
  • When a device connects, it sends its delivered_seq per conversation (or one "sync token"), and the service returns everything newer, newest conversations first. History older than what the device holds is fetched only when the user scrolls (cursor pagination by seq).
  • Gap detection: if a device receives seq 1045 but last had 1042, it fetches 1043 and 1044 before displaying in order.

Multiple devices

#

Each device has its own cursor in device_cursors. A message is delivered to every device of each member, including the sender's other devices (so the conversation looks the same on the sender's laptop). Read state syncs across devices: reading on the phone clears the unread badge on the laptop.

Group chats

#

A group message is stored once (in the conversation's partition) and delivered to each member's devices. With up to 500 members, fan-out is at most a few thousand device deliveries per message, handled asynchronously by fan-out workers in batches. For very large groups, receipts become expensive (500 "read" receipts per message); systems often aggregate them ("read by 12") or disable per-person receipts above a size.

Presence and typing indicators

#

Presence ("online", "last seen 5 minutes ago") changes constantly. Pushing every change to every contact is costly: a user with 300 contacts going online and offline 50 times a day generates 15,000 updates. Keep it cheap:

  • Presence derives from the session registry (a device is online if its registry entry is fresh).
  • Fetch presence on demand when a user opens a chat, and subscribe only for the chats currently open on screen.
  • Typing indicators are ephemeral frames relayed to online members of an open conversation, never stored, rate-limited (at most one every few seconds per user).
  • Treat both as lossy and eventually consistent; nothing bad happens if one is missed.

Media

#

Photos and videos are uploaded directly to object storage with a presigned URL (article 10; presigned URLs); the message carries only the object key and a thumbnail. Recipients download through a CDN with short-lived signed URLs, so only conversation members can fetch media.

Storage at scale

#

Messages go to a wide-column store partitioned by (conversation_id, bucket), replicated three ways across zones, with writes acknowledged by a quorum (article 19); Apache Cassandra, for example, documents this Dynamo-style partitioning and tunable consistency (Cassandra: Dynamo). The relational database keeps accounts, conversations and memberships, where relationships and constraints matter. Old buckets can move to cheaper storage tiers.

9. Failure scenarios and reliability

#

Failure

What happens

Response

Gateway crashes (100,000 connections)

Devices disconnect

Devices reconnect with exponential backoff and jitter to other gateways (article 05); they sync from their cursors; registry entries for the dead gateway expire

ACK lost on the way to the sender

Sender retries

client_msg_id deduplication returns the same seq; no duplicate

Message service crashes after storing, before ACK

Sender retries

Dedupe record (written with the message, or derived from the stored client_msg_id) returns the stored seq

Real-time delivery dropped

Recipient misses a frame

Gap detection or sync on reconnect fetches it from storage

Registry stale (device moved to another gateway)

Delivery to the old gateway fails

Look up again; fall back to push plus sync

Message store partition unavailable

Sends to affected conversations fail

Replicas with quorum writes; sender sees "sending" and retries

Push provider down

Offline users not alerted

They still receive messages when they open the app (sync)

Deployment of gateways

Mass disconnects

Drain gradually; ask clients to reconnect over minutes

Region failure

Users in that region disconnected

Reconnect to another region; messages replicated across regions asynchronously, with a small loss window (article 24)

10. Consistency

#
  • Within a conversation: a total order defined by seq, identical on every device.
  • Across conversations: no ordering guarantee is needed.
  • Delivery: at-least-once to devices; devices deduplicate by (conversation_id, seq), so users never see duplicates.
  • Read-your-writes: the sender's device shows its own message immediately (optimistically), confirmed by the ACK; its other devices receive it like any member.
  • Receipts, unread counts, presence: eventually consistent.
  • Durability: a message is acknowledged only after it is written to a quorum of replicas.

11. Security

#
  • Transport security: TLS on all connections.
  • Authentication at WebSocket connect (a short-lived token), with periodic re-validation; revoked sessions are disconnected.
  • Authorisation on every send and every history read: the user must be a current member of the conversation (object-level checks, article 28). Removing a member must stop future deliveries to them.
  • Abuse: rate limits on sends per user and per new account; spam detection on content and patterns; limits on group creation and invites; report and block features.
  • Media: scanned for malware; signed, short-lived download URLs.
  • Privacy: presence and last-seen visibility controlled by user settings; minimal metadata in logs.
  • End-to-end encryption (extension): encrypting messages so that only participants' devices hold keys changes the design significantly: the server stores ciphertext, cannot search or moderate content, and multi-device and group key management become the hard problems. Mention it as a deliberate scope decision.

12. Observability

#

Signal

Why

Send-to-ACK latency, and send-to-delivery latency for online recipients

The core SLOs

Concurrent connections per gateway; connect and reconnect rates

Capacity, and reconnect storms

Fan-out queue age

Delivery delays

Push fallback rate

How many deliveries rely on push

Gap fetches and sync volume

Reliability of the real-time path

Message store write latency and errors per partition

Hot partitions, storage health

Dedupe hits

Retry behaviour of clients

13. Trade-offs

#

Decision

Chosen

Alternative

Why

Transport

WebSockets for real-time, HTTP for history

Long polling

Two-way, low-latency, efficient for frequent small messages

Gateways

Thin, stateful, separate from logic

Logic in gateways

Fewer redeploys, fewer mass disconnects

Ordering

Per-conversation sequence assigned in one place

Timestamps

Clocks disagree; sequences enable gap detection and deduplication

Delivery

At-least-once plus client deduplication

Exactly-once attempts

Simple and robust under constant disconnects

Store

Wide-column, partitioned by conversation and bucket

Sharded relational

Write volume and the single dominant access pattern

Group fan-out

Store once, deliver per device

Copy per recipient (inbox per user)

Saves storage; sync by sequence

Presence

On demand and subscribed, lossy

Broadcast every change

Avoids huge fan-out for a low-value signal

14. The final interview answer

#

"We support one-to-one and group chats for 100 million daily users: about 92,000 messages a second at peak, roughly 550,000 device deliveries a second, and 39 million concurrent connections.

Devices hold WebSocket connections to thin, stateful gateways, about 400 of them across zones. Gateways register each device in a Redis session registry with heartbeat TTLs. A stateless message service handles each send: it checks membership, deduplicates by the client's message id, assigns a per-conversation sequence number in one place, writes the message to a wide-column store partitioned by conversation and month with quorum replication, and only then acknowledges the sender.

Fan-out looks up each member's devices in the registry and routes the message to their gateways; offline devices get a push via the notification platform and sync from their last delivered sequence when they reconnect. Sequence numbers give the same order on every device, let devices detect gaps, and make retries harmless, so delivery is at-least-once with client-side deduplication. Presence and typing are lossy and fetched on demand. If a gateway dies, clients reconnect with jittered backoff and resync, so nothing acknowledged is lost."

Exercises

#

Exercise 1

Gateways and memory

#

You expect 39 million concurrent connections. Each connection uses about 30 KB of memory on a gateway (buffers, TLS state, session data), and each gateway has 32 GB of memory available for connections. How many connections can one gateway hold by memory, how many gateways do you need, and what else might limit connections per gateway?

Exercise 2: Reconnect while sending

#

A user sends a message, the connection drops before the ACK arrives, and the app reconnects to a different gateway 3 seconds later. Walk through exactly how the design ensures the message is stored once and shown once on every device.

Exercise 3: A 100,000-member group

#

Product wants groups of up to 100,000 members where anyone can post. What breaks in this design, and what would you change?

Exercise 4: Presence cost

#

Each user has on average 200 contacts, goes online or offline 40 times a day, and 100 million users are daily active. Estimate the presence updates per second if every change were pushed to every contact. Then estimate the load with "subscribe only to chats currently open on screen", assuming an online user has on average 1 chat open, with 2 participants.

Show solutionHide solution
Code
By memory: 32 GB / 30 KB = 32 x 10^9 / 30 x 10^3 = about 1.07 million connectionsGateways for 39 million: about 37 (by memory alone)

Memory is not the binding limit here. Others are: CPU for TLS handshakes and heartbeats (especially during reconnect storms), file descriptors and kernel network buffers, traffic per connection (active chats produce frames constantly), and the blast radius of one gateway failing (1 million users reconnecting at once). A practical target such as 100,000 connections per gateway (about 400 gateways) leaves headroom, keeps reconnect storms smaller, and spreads load across zones; the exact number comes from load testing.

Exercise 2

Show solutionHide solution
  1. The device sent SEND{client_msg_id: c-7f3a}; the message service checked dedupe (none), assigned seq 1042, stored the message, recorded (cv_88, c-7f3a) -> 1042, and sent the ACK, which was lost with the connection.
  2. The device still shows the message as "sending" (it never received the ACK). On reconnecting to a new gateway, it re-sends SEND{client_msg_id: c-7f3a}.
  3. The message service finds the dedupe record and replies ACK{c-7f3a, seq 1042} without storing anything new. The device marks the message sent with seq 1042.
  4. Meanwhile, fan-out had already delivered seq 1042 to other members (or they will receive it on sync). Because devices keep messages keyed by (conversation_id, seq), even if a recipient receives seq 1042 twice (real-time delivery and a sync), it shows once.
  5. The sender's own other devices receive seq 1042 through normal fan-out and show it once.

The key ideas: idempotent sends via client_msg_id, ordering and identity via seq, and deduplication at the client.

Exercise 3

Show solutionHide solution

What breaks:

  • Fan-out per message jumps to 100,000 members times their devices: one popular group can generate hundreds of thousands of deliveries per message, and many messages per minute.
  • Per-member receipts become meaningless and very expensive.
  • Sequence assignment for one conversation becomes a hot key if many members post at once.
  • Push notifications to 100,000 offline members per message would be a flood.

Changes (this is the "broadcast channel" design, explicitly out of scope originally):

  • Switch delivery for large groups from push to pull: online members whose screen shows the group get live updates through a per-group subscription; others see an unread count and fetch messages when they open the group.
  • Aggregate receipts (counts only) or drop them.
  • Rate-limit posting per member and per group, or restrict posting to admins.
  • Batch and throttle push notifications (one summary push, not per message), honouring mute settings.
  • Shard the group's sequence assignment carefully (it remains single-writer, but at controlled posting rates).

Exercise 4

Show solutionHide solution

Push every change to every contact:

Code
100M users x 40 changes/day x 200 contacts = 800 billion updates/day/ 86,400 = about 9.3 million updates per second

That is far more than the message traffic itself, for a low-value signal.

Subscribe only to open chats: an online user has about 1 chat open with 2 participants, so each presence change matters to about 1 other user (the other participant, if they have that chat open). Roughly 100M x 40 / 86,400 = about 46,000 presence changes per second, each sent to about 1 subscriber: about 46,000 updates per second, a reduction of about 200 times. Presence for the chat list can be fetched on demand when the list is opened, and cached briefly.

Where to go next

#

Sources & further reading

  1. rfc-editor.org
  2. redis.io
  3. kafka.apache.org
  4. cassandra.apache.org
  5. firebase.google.com
  6. docs.aws.amazon.com
  • Guide · Technology

    Start Here: Your Roadmap from Zero to Job-Ready DSA

  • Guide · Technology

    Heaps and Priority Queues

  • Guide · Career

    A LinkedIn Profile That Gets Recruiter Messages