Chat, where order and 'delivered' have to mean something
Millions of long-lived connections, messages that must appear in the same order for everyone in a conversation, and a phone that goes offline in a tunnel mid-send.
The brief
Users send messages in one-to-one and group conversations, see them arrive in real time, and see messages they missed when they come back online.
Users have several devices, and every device must show the same conversation.
Requirements
Functional
- Send and receive messages in real time in one-to-one and group conversations
- Deliver messages sent while a device was offline when it reconnects
- Show the same order of messages to every participant and every device
- Show sent, delivered and read states
Non-functional
- A message the sender saw as sent must never be lost
- A message must not appear twice, even when the sender's phone retried it
- Real-time delivery under a second for online recipients
- Losing a connection server must not lose messages, only connections
Back-of-envelope
Assume
- 50 million daily users, 20 million connected at peak
- 40 messages sent per user per day, average 200 bytes
- Groups capped at 500 members
- A connection server holds 100,000 idle WebSocket connections
Therefore
- Messages: 5e7 × 40 = 2e9 a day ≈ 23,000 a second on average, perhaps 70,000 a second at peak.
- Storage: 2e9 × 200 bytes = 400 GB a day of message bodies, before replication and indexes — about 150 TB a year. Retention is a cost decision, not a default.
- Connections: 2e7 / 1e5 = 200 connection servers at peak, before headroom. The fleet is sized by connections held, not by messages sent.
- A message to a 500-member group is 500 deliveries. One busy group sending a message a second generates as much delivery work as 500 one-to-one conversations.
Assume 100,000 connections per server is optimistic for a server that also does TLS and heartbeats; halve it and the fleet doubles. Measure the real number before believing the third line.
The interface
What is stored
partition: conversation_id · clustering: seq · message_id · sender_id · client_message_id · body · sent_atEvery read is 'messages in this conversation after seq N', which is a range scan within one partition. A wide-column store fits that access pattern exactly; a query across conversations is never needed on the hot path.
conversation_id · next_seqAssigning seq needs a per-conversation atomic increment. It serialises writes within one conversation — which is fine, because a single conversation's write rate is human-sized.
user_id → [{ deviceId, connectionServer }] with a TTL refreshed by heartbeatDelivery needs to know which server holds each device's socket. The TTL means a crashed server's entries disappear on their own rather than routing messages into a dead socket forever.
The design
The decisions
Each of these could go the other way. The choice, the reason, and what it costs — a design that lists only what it chose teaches the choice; one that lists what it gave up teaches the judgement.
What breaks first
In order. Each names what you would actually observe, and each fix carries its cost.
When something fails
Scaling it
Each step is triggered by a number, not a feeling — and carries what it costs.
What gets probed
The design is the easy half. These are where the conversation goes, and each has a defensible answer above.
- A phone sends a message, the ack is lost, and the phone retries. Show why the recipient sees it once.
- Two people in a group send a message at the same moment. Why does every device show them in the same order?
- A connection server dies. What is lost, what is late, and what is untouched?
- What exactly does the 'delivered' tick promise, and who is allowed to set it?