Design WhatsApp — WebSockets, inbox, and pub/sub routing

Design WhatsApp

Messaging walkthrough: WebSocket commands, group chats (cap 100), durable inbox for offline delivery, presigned media, Redis pub/sub across chat servers, multi-device clients, heartbeats, and last-seen — with interview bars by level.

Understanding the problem

WhatsApp is a messaging service for send/receive of messages (and calls) from phones and computers. Interviewers don't want every Messenger feature — lock the core path, then deep-dive deliverability and scale. Ask how the product is used (mostly 1:1 vs large groups, messages/day) — those answers drive partitioning later.

Online walkie-talkie delivery, durable postbox inbox for offline, and many chat servers bridged by pub/sub.
Live when online · postbox when offline · pub/sub when hosts differ.
01Chats

2–100 participants.

02Inbox

Offline ≤30 days.

03Pub/Sub

Cross-host delivery.

04Media

Presigned blobs.

Pair with Delivery framework, Real-time updates, Large blobs, Redis, and News feed (celebrity / fan-out cousin).

Functional requirements

Whiteboard functional and non-functional requirements for WhatsApp.
Don't burn the clock inventing out-of-scope features — nice-to-have only.

Non-functional requirements

Planning the approach

Recognize two halves early: durable delivery (Inbox / Messages) and realtime push (WebSockets + pub/sub). Solve the general group case; 1:1 falls out. Start single-host so mechanics are clear — then scale out. Scaling first without understanding delivery is how you paint yourself into a corner.

  1. Entities + WebSocket command API.
  2. HLD per FR: create chat → send → offline inbox → media.
  3. Deep dives: multi-host pub/sub, multi-device, heartbeats, gap detection, last-seen.
  4. Level check: mid owns breadth; senior owns routing/robustness; staff drives failure modes.

Core entities

Nouns for the rest of the interview — not a grading rubric, but a broken foundation if wrong.

Users, Chats, Messages, and Clients entities.
Clients matter once multi-device shows up — introduce them in the deep dive.

API — WebSocket commands

Chat is high-frequency bi-directional traffic — a poor fit for request/response REST. Use WebSockets over TLS (or a custom protocol over TLS TCP). Clients open a socket and exchange commands. Pattern deep dive: Real-time updates.

Client-to-server and server-to-client WebSocket commands for WhatsApp.
In interview: command names may be enough — full JSON is optional.
// -> createChat { participants[], name } -> { chatId }
// -> sendMessage { chatId, message, attachments[] } -> { status, messageId }
// -> getAttachmentTarget { ... } -> { uploadUrl }
// -> modifyChatParticipants { chatId, userId, ADD|REMOVE }
// <- chatUpdate / newMessage  ... client replies RECEIVED (ACK)

HLD — create group chats

Chat Service behind an L4 load balancer (WebSockets; no L7 path routing needed). Persist Chat + ChatParticipant rows in DynamoDB (or similar KV).

Client createChat through L4 LB to Chat Service writing Chat and Participants in DynamoDB.
createChat → Chat + ChatParticipant writes → return chatId.
  • Chat — primary key chatId.
  • ChatParticipant — PK chatId, SK participantId (list members).
  • GSI — PK participantId, SK chatId (list a user's chats).
  • Near 100 participants, batch writes; small chats can use a single transaction.

HLD — send/receive (single host first)

Start with one Chat Server. Say out loud that it's not scalable — then keep the mechanics clean. In-memory map: userId → WebSocket.

  1. Client sends sendMessage.
  2. Lookup participants via ChatParticipant.
  3. Push newMessage on each participant's socket (assumes everyone online on this host).

HLD — offline delivery (Inbox)

Persist messages and keep an Inbox of undelivered message IDs per recipient. Write durability before attempting push.

Client WebSocket through L4 LB to Chat Server to DynamoDB with Chat, ChatParticipant, Inbox, and Message schemas.
Send a Message — WS → L4 LB → Chat Server → DynamoDB (Inbox + Message).
  1. Write Message row + Inbox entries for each recipient.
  2. Return SUCCESS + messageId to sender.
  3. Attempt newMessage on connected sockets; clients ACK → delete Inbox rows.
  4. On connect: drain Inbox → fetch Messages → push → ACK → delete.
  5. TTL on Inbox/Messages (~30 days) for cleanup.

HLD — media attachments

Media is bandwidth- and storage-heavy. Don't shove blobs through the chat WebSocket into DynamoDB. WhatsApp-style: attachments via a separate HTTP/blob path.

Client to Chat Server to Database for attachments stored in the DB.
Attachments in DB — worst path; cripples chat hosts and fights the database.
Chat Server writes media to blob storage; client reads media from blob; messages still go to the database.
Use Blob Storage — better, but Chat Server still handles the upload hop.
Client retrieves upload target from Chat Server then reads and writes media directly to blob storage.
Use Presigned URLs — client uploads direct; message carries an opaque URL.
  • Bad — media over WS into DB (cripples chat hosts; DBs hate big binaries).
  • Better — Chat Server forwards to blob store (TTL 30d); still a wasted hop.
  • BestgetAttachmentTarget returns presigned upload URL; client uploads direct; message carries opaque URL; recipients download via presigned GET. CDN optional (max 100 participants).

Same pattern as Handling large blobs / Dropbox.

Deep dive — billions of users / multi-host

~1B users → ~200M connected. Lore: WhatsApp ~1–2M users/host → hundreds of chat servers. Sender and recipient often land on different hosts — in-memory maps don't help.

User A sends via Chat Server 1 to User B successfully but cannot reach User C on Chat Server 2.
Host Confusion — A and B on Server 1; C on Server 2 never gets the message.
Users A B C across two chat servers bridged by Redis Pub/Sub per userId.
Fix: Redis Pub/Sub (channel per userId). Inbox still written first.

Partition by user vs chat? WhatsApp is 1:1-dominated with a hard 100-participant cap. Per-user channels avoid hundreds of redundant chat subscriptions. For large chats (e.g. >25), adaptively also subscribe to a chat-level channel and publish there — briefly dual-publish while membership converges. Celebrity-problem cousin of news-feed fan-out.

Alternatives (sticky LB, consistent hash of user→server) live in Real-time updates. Pub/Sub is the usual interview default once you leave one box.

Deep dive — multiple clients per user

Phone got the message; laptop was asleep — laptop must catch up. User-level Inbox is no longer enough.

Adding clients: WS to L4 LB to Chat Servers with Redis Pub/Sub, DynamoDB, Clients table and per-client Inbox.
Adding clients — Clients table · Inbox keyed by recipientClientId · pub/sub still per userId.
  • Add Clients table keyed by userId.
  • Resolve participants → all active clients; deliver + ACK each.
  • Inbox becomes per-client.
  • Deactivate dead clients so you don't store forever.
  • Pub/Sub topics unchanged (still userId).

Deep dive — dead sockets, lost pub/sub, order

Heartbeats, sequence gap detection, and backstop polling for chat reliability.
Heartbeats detect dead WS · seq in ping finds gaps · rare poll as backstop.
  • Dead WebSocket — TCP keepalive is minutes; too slow. Prefer ping/pong every 10–30s (close on miss). Also: ACK timeout on delivery (500–2000ms) then retry / close.
  • Redis Pub/Sub loss — at-most-once. Durability = Inbox-before-publish. Detect gaps with per-user monotonic seq (Redis INCR); include seq on heartbeats; client syncs Inbox. Periodic poll as backstop (tune interval vs load).
  • Out-of-order — don't wait for perfect order. NTP-synced servers stamp receive time; clients display by server timestamp. Occasional "pop-in above" is acceptable UX.

Deep dive — last seen

Don't write lastSeen on every heartbeat — that's a write storm for stale data.

Avoid heartbeat writes; persist last seen on disconnect and probe online via pub/sub.
Persist on disconnect · probe ONLINE via pub/sub · client merges answers.
  1. On disconnect, conditional-update LastSeen table (only if newer timestamp).
  2. Client sends getLastSeen; server reads DB and publishes probe to target user channel.
  3. If target host has an open socket, reply ONLINE; else show disconnect time.
  4. Client merges DATABASE vs SERVER reporters (ONLINE wins).

Final design

Final WhatsApp design with L4 LB, many chat servers, Redis pub/sub, DynamoDB, and S3 presigned media.
L4 → chat server fleet · Redis pub/sub + seq · DynamoDB Chat/Inbox/Messages · S3 media.

Cost and performance levers

What interviewers expect by level

Mid senior and staff expectations for WhatsApp system design.
Deliverability + connection density beat inventing Kafka for chat text.
Interview takeaway

Mid: working spine + offline inbox. Senior: multi-host + robustness. Staff: anticipate failures and keep the design lean like WhatsApp's ops reputation.

Wrapping up

WhatsApp interviews reward deliverability under flaky networks and connection-dense scale, not feature laundry lists. Inbox-before-publish, client ACKs, pub/sub across hosts, and media off the chat path get you most of the way — then harden with heartbeats, sequences, and multi-device inboxes.

Related: Delivery framework · Real-time updates · Large blobs · Redis · News feed · Networking · Common patterns.

← Lattice