Each phone keeps an open line to a switchboard. When you send a message, the switchboard saves it in a logbook first, tells you it's received, then connects to your friend's line. If your friend is offline, the message waits in the logbook and a buzz (push notification) tells them.
Design a chat app like WhatsApp or Messenger: one-to-one and small group conversations with messages delivered in real time when the recipient is online, stored and delivered later when they're offline, with sent/delivered/read receipts and online presence.
Messages: 500M x 40 = 20B/day -> ~230K/s avg, ~600K/s peak
Storage: 20B x ~200 B = 4 TB/day -> write-heavy, append-only
Connections: 50M concurrent / ~500K per gateway server -> ~100+ gateway servers
Real-time channel: a WebSocket per device, frames in both directions
-> { type: "send", client_msg_id, conversation_id, body }
<- { type: "ack", client_msg_id, message_id, ts } (server stored it)
<- { type: "message", message_id, conversation_id, sender, body, ts }
-> { type: "delivered" | "read", conversation_id, up_to_message_id }
REST for everything else
GET /v1/conversations/{id}/messages?before=<message_id>&limit=50 (history)
POST /v1/groups { name, member_ids } GET /v1/sync?since=<cursor>
device A ==ws==> Gateway 1 Gateway 7 <==ws== device B
| ^
v |
Chat service ---------> route to B's gateway
1. dedupe (client_msg_id) (connection registry:
2. assign message_id user/device -> gateway, in Redis)
3. persist to message store
4. ack to A
5. online? -> deliver via B's gateway
offline? -> push notification service (APNs/FCM)
Message store: wide-column DB (Cassandra/HBase-style), partitioned by conversation_id
Presence service: heartbeats -> Redis with TTL
Group fan-out: chat service delivers to each member's gateway (groups are small)
messages
PARTITION KEY conversation_id
CLUSTERING KEY message_id DESC -- newest first, cheap "latest 50"
columns: sender_id, body, created_at, client_msg_id
message_id: time-sortable and increasing per conversation
(e.g. snowflake, or a per-conversation sequence assigned by the one chat
server that owns the conversation) -> total order within a conversation
Partitioning by conversation keeps each conversation's history on one partition and makes "load last 50" a single sequential read. Very large, long-lived groups can bucket by time (conversation_id + month) to cap partition size.
client_msg_id, so retries never duplicate.sync?since=cursor and receives everything it missed, so the WebSocket is an optimisation and the store is the source of truth.on connect: SET conn:{user}:{device} = gateway_7 (TTL, refreshed by heartbeat)
deliver(msg): for each device of recipient:
gw = GET conn:{user}:{device}
if gw: send msg to gw via its inbox channel (pub/sub or RPC)
else: enqueue push notification
on disconnect: DEL conn key; last_seen = now
Clients heartbeat every ~30 s; presence is a Redis key with a TTL slightly longer. Presence changes are sent only to users who currently have the chat open (subscribe on open), not broadcast to all contacts, which would be enormous fan-out.