Skip to main content

Design Facebook Messenger

Premium

You're asked to design a one-to-one and small-group messaging product like Facebook Messenger or iMessage. A user sends a message and it appears on the recipient's phone within a second if they're online, or lands as a push notification if they aren't. Both people see when a message was delivered and when it was read, conversations sync across a phone, a tablet, and a laptop, and the full history is available on any of them.

Slack sends one message to large channels. Messenger usually sends to one or two recipients, but it has billions of conversations and several devices per user. Let's focus on per-device delivery state, receipts, and history sync.

Clarify the requirements

  • How large can a group be? Small groups, up to a few hundred, is the usual scope and it keeps fan-out manageable. Confirm you're not being asked for broadcast channels.
  • Do we need read receipts? Assume yes, and establish this early because per-device receipt state affects the data model.
  • How many devices per user? Multi-device is the assumption in any modern version of this question, and it changes the delivery model substantially.
  • Is history stored on the server? Yes for this version. Server-stored history is what makes a new device usable immediately, and it's the assumption that Design WhatsApp removes.
  • Is media in scope? Photos and video dominate storage but are a solved sub-problem: object storage, presigned upload, reference in the message.

Assume: groups up to a few hundred, delivery and read receipts, several devices per user, server-stored history.

Back-of-envelope numbers

  • Users: 1B daily actives1\text{B daily actives}, with roughly 500M500\text{M} connected at peak
  • Connections: at 100k100\text{k} sockets per gateway, 500M÷100k=5,000 gateways500\text{M} \div 100\text{k} = 5{,}000 \text{ gateways} plus headroom
  • Messages: 100B messages/day1.2M msg/sec100\text{B messages/day} \approx 1.2\text{M msg/sec} average, with peaks around 5M/sec5\text{M/sec}
  • Storage: 100B×300 B30 TB/day100\text{B} \times 300\text{ B} \approx 30\text{ TB/day}, or roughly 11 PB/year11\text{ PB/year} of text alone
  • Receipts: with delivered and read events per recipient per device, receipt traffic is a small multiple of message traffic and can't be treated as free

The storage figure is the one that constrains the design. Eleven petabytes a year of small records means the message store's partition key has to make the common query cheap, because no amount of hardware rescues a bad one at that size.

High-level architecture

Sender device

① Connection gateway

② Chat service

③ Message store

④ Session registry

⑤ Delivery queue

⑥ Recipient gateways

⑦ Push service

Recipient devices

Components
  1. Connection gateway. Holds the persistent WebSocket for each connected device.
  2. Chat service. Assigns the message its identity and ordering, persists it, and drives delivery.
  3. Message store. Partitioned by conversation, since every read is scoped to one.
  4. Session registry. Which devices are currently connected, and to which gateway.
  5. Delivery queue. Decouples the sender's write from N recipient deliveries.
  6. Recipient gateways. Push to devices holding an open connection.
  7. Push service. APNs or FCM for devices that aren't connected.
Devices hold a persistent connection to a gateway. The chat service writes the message once, then delivers to each recipient device that's connected and queues for the rest.

Deep dive 1: storing messages once, delivering many times

The first real decision is whether a message is stored once per conversation or once per recipient, and it's worth reasoning through both rather than asserting one.

A copy per recipient, the "fan-out on write" inbox model, makes each user's unread list a single-partition read and lets per-user state live on the copy. It costs storage proportional to group size and makes editing or deleting a message an N-row update.

One copy per conversation stores the message once, partitioned by conversation ID, with recipients reading from the shared timeline. Storage is minimal, edits and deletes touch one row, and history for a conversation is a contiguous range scan, which is exactly the query the product makes constantly.

For groups of a few hundred, let's store one message body per conversation. The body is immutable and shared, while each user's state is small. A lightweight per-user, per-conversation cursor can hold the last read message ID without duplicating the body.

That cursor does a lot of work for its size. Unread count is derivable from it, "jump to where I left off" is a lookup, and marking a conversation read is a single write regardless of how many messages it covers.

Partition by conversation ID. Every read is "give me the last N messages in this conversation," which stays inside one partition. Ordering within it comes from a sequence number the chat service assigns, not from client timestamps, whose clocks disagree.

Deep dive 2: multi-device delivery

A user with a phone, a tablet, and a laptop has three destinations for every message, and treating the user as the unit of delivery breaks as soon as they own a second device.

The device is the delivery target, not the user. The session registry maps a user to their currently connected devices and the gateway holding each one. Delivery looks up every device for every recipient, pushes to the connected ones, and queues for the rest.

That model has two consequences:

  • The sender's other devices are recipients too. Sending from your phone has to make the message appear on your laptop, which means the sender's own device list is part of the fan-out.
  • A device that was offline for a week needs to catch up, and it does so with the same mechanism a device offline for a minute uses: it reconnects, sends the last message ID it holds per conversation, and receives everything after. No special path, which is the property that makes it reliable.

Undelivered messages need a per-device queue with a retention window. A device that never comes back shouldn't accumulate messages forever, and a device that returns after a month should get history from the message store rather than from a queue.

Deep dive 3: delivery and read receipts

Receipts look like a small feature and are a substantial part of the traffic and the state.

A message moves through four states, and each transition is an event travelling in the opposite direction from the message:

  • Sent. The server has accepted and persisted it. Acknowledged on the sender's own connection.
  • Delivered. It reached a recipient device. Reported by that device on receipt.
  • Read. The recipient opened the conversation. Reported when the view is displayed.

The complication is multi-device. A message delivered to a phone but not a laptop is partially delivered, and the product has to pick a rule: most show delivered when any device has it and read when any device has read it. Let's state that rule explicitly because the alternative interpretations are equally defensible.

Receipts must be batched. A user scrolling through fifty unread messages generates fifty read events if you send one per message, which triples your traffic to communicate very little. Send the highest message ID read per conversation instead, on a short debounce. One event covers the whole batch, and it composes naturally with the cursor from the first deep dive.

Group receipts don't scale the same way. In a two-person chat, showing exactly who has read it is cheap. In a two-hundred-person group it's two hundred receipt events per message and a UI nobody reads. Showing an aggregate count, or nothing at all above a size threshold, is the standard resolution.

Deep dive 4: connections, presence, and reconnection

Half a billion concurrent connections is the operational reality behind the product, and the gateway tier is where that cost lives.

Gateways are stateful and should do nothing else. Their job is to hold sockets and route frames, which keeps them cheap per connection and lets the stateless chat service scale independently. The session registry is what lets any part of the system find the gateway holding a given device.

Presence is deceptively expensive. Naively, every user's online status is pushed to everyone who might see it, which is quadratic in a dense social graph. We can control that cost in three ways: only compute presence for people you're actively looking at rather than your whole contact list; batch and debounce transitions so a flaky connection doesn't emit a flurry of on/off events; and accept a coarse granularity, since "active recently" is what the product actually shows.

Reconnection is the common case, not the exception. Mobile clients lose connectivity constantly, in a tunnel, a lift, or a network handover. The client should reconnect with exponential backoff and jitter, because a regional network blip otherwise reconnects millions of devices simultaneously and the retry storm becomes a worse outage than the original one.

Common pitfalls

  • Storing a copy of every message per recipient. Storage multiplies by group size and edits become N-row updates, when a per-user cursor gives you the same product behavior.
  • Ordering by client timestamp. Device clocks disagree, and messages arrive out of order.
  • Treating the user as the delivery target. Multi-device breaks immediately.
  • One receipt event per message. Reading a backlog generates a burst of traffic for information one event conveys.
  • Reconnecting without backoff and jitter. A brief network blip becomes a self-inflicted thundering herd.

Leveling signals

Mid-levelUses persistent connections through a gateway tier, stores messages partitioned by conversation, and falls back to push notifications when a recipient is offline.
SeniorStores the body once with a per-user read cursor rather than duplicating per recipient, makes the device the unit of delivery with a session registry, and syncs a reconnecting device from its last known message ID.
Staff+Defines receipt semantics explicitly across multiple devices and batches them per conversation. Treats presence as a cost to bound rather than a feature to maximize, and requires backoff with jitter so a network blip doesn't become a reconnection storm.
Design WhatsAppHard

Add end-to-end encryption, and design a server that can route messages it cannot read.

Design SlackHard

Deliver messages to large channels, where one message fans out to many thousands of members.