Skip to main content
Chat System (WhatsApp / Slack)lesson 4 of 4 · 2 min read

Groups, Receipts, and Presence

Where the multiplication happens

Group chat turns one send into many deliveries, and your design question is where that multiplication happens.

Take a 200-member group. The sender's server writes once to the group conversation and publishes one event, each member's connection server picks it up and pushes to its local sockets, and offline members catch up through their cursors.

See why the storage model matters here: the message is stored once, not 200 times.

The messaging apps cap group size, and partly for this reason. Receipts and delivery bookkeeping scale with your membership, not with your messages.

Model receipts as a little state machine. Sent means your server saved it, delivered means a recipient device acknowledged it, read means the conversation was on screen.

Push each transition back through the same channel, and watch the bookkeeping get nearly quadratic. A read receipt in a 200-member group is 200 tiny events per message per reader.

Aggregate them into counts rather than a row per person, at least until somebody opens the details panel, or your receipts will out-write your messages.

Presence, the classic trap

Treat presence as the classic scale trap it is, because it looks trivial. Broadcast every online and offline flip to all contacts and you melt the moment a phone on a flaky train connection flaps every few seconds.

Use heartbeats with lazy expiry instead. Your client pings every 5 seconds, your server sets a key with a 10 second expiry, and presence reads check that key on demand.

Broadcast only debounced transitions, and only to people currently looking at a screen where that presence appears. Nobody needs to know within a second that a contact they have not opened in months went offline.

the shape of it
One sendGroup conversationstored onceServer 3Server 9Offline memberscursor sync1. write once2. push to live3. push to live4. caught up later
step 1 of 4
The message is stored once and pushed to whichever servers hold live sockets.

Worked example

Fatima owns presence at a workplace chat product with 3 million concurrent users. The v1 design publishes every status change to every teammate, and one Monday a mobile carrier hiccup makes 200,000 phones reconnect repeatedly for 10 minutes. Each flap fans out to an average of 40 coworkers, and the pub/sub tier peaks at 900,000 presence events per second, starving actual messages, which queue up 20 seconds behind. Messages beaten by green dots is the incident title. Her redesign: clients heartbeat every 5 seconds into Redis keys with 10 second TTLs, transitions are debounced for 30 seconds before broadcast, and clients subscribe to presence only for the roster currently rendered on screen. The same carrier flap a month later produces 4,000 events per second and no user-visible impact.

Chat System (WhatsApp / Slack): wrapping up

In the real world

  • 01WhatsApp famously served hundreds of millions of users with around 50 engineers, running Erlang on FreeBSD and demonstrating over 2 million TCP connections on a single server in its 2012 engineering posts.
  • 02Discord stores trillions of messages keyed by channel and time-sortable ID, and documented its 2023 migration from Cassandra to ScyllaDB after garbage collection pauses on hot partitions caused cascading latency.
  • 03Slack's clients boot through Flannel, an edge cache service that holds a warm copy of team state near users, because replaying a large team's full state over a fresh WebSocket on every reconnect did not scale.
  • 04WhatsApp's double and blue tick receipts are the canonical sent, delivered, read state machine, and its group size limits grew slowly (256 to 1,024) because receipt bookkeeping scales with member count.
  • 05Signal delivers over the same push-then-sync pattern but stores as little as possible server-side; messages queue encrypted until devices fetch them, showing how retention requirements reshape the storage tier.

Questions people ask

Why WebSocket instead of polling or server-sent events?

Polling adds latency (up to the poll interval) and wastes battery and server capacity on empty responses. Server-sent events are one-directional, server to client, so sends still need separate HTTP requests. WebSocket gives one bidirectional connection for both directions, which is why every major chat product uses it or an equivalent persistent socket protocol.

What happens to a message if the recipient's chat server crashes?

Nothing is lost, because the message was persisted before the publish. The recipient's client detects the dead socket, reconnects through the load balancer to a healthy server, and syncs from its per-conversation cursor, picking up anything published while it was disconnected. This is why persist-then-publish ordering and cursor-based sync are the backbone of reliability, not the pub/sub layer.

How do you keep messages in order?

Guarantee order per conversation only, using a time-sortable server-assigned message ID as the single source of truth. Clients render by ID order, and any message arriving out of order slots into place. Global ordering across conversations is unnecessary and would require coordination that kills throughput.

Quick review

WebSocket:
persistent connection per user to a chat server. Server can push messages without polling
Horizontal scaling problem:
User A on server 1, User B on server 2 → servers can't communicate directly
Solution:
Redis Pub/Sub or Kafka as message relay. Every server subscribes to user-specific topics
Message storage:
Cassandra (wide-column) with (channel_id, timestamp) as primary key. Append-only, fast writes
Offline delivery:
store messages in DB when recipient offline. On reconnect, pull unread since last_seen_timestamp
Message delivery receipts:
Sent (stored in DB), Delivered (reached device), Read (user saw it), like WhatsApp double/blue ticks
Group chat fan-out:
for each group message, write to each member's inbox (fan-out on write) OR store once and pull (fan-out on read)
Presence service:
user sends heartbeat every 5s. Redis TTL key per user_id. Expiry = offline
the trade-off

Fan-out on write: expensive for large groups (1000+ members). Fan-out on read: slower opens. Hybrid for groups > N.

in the room

Tests real-time protocols, stateful connection scaling, offline delivery, and storage for time-ordered data.