The big idea
What is Slack?
Team chat: people post to channels, and every member sees the message in real time, across all their devices, with full searchable history. The twist versus 1:1 messaging is fan-out to groups and durable, organized history per channel.
Persist every message to a per-channel store, deliver in real time over WebSockets, and fan a message out to all channel members — bridging servers with a pub/sub bus. Then layer presence, search and notifications on top.
How to read this: We add one piece at a time, problem then fix, and the diagram grows. Hit Begin.
Step 1 · The skeleton
Post to a channel, persist
A message is sent to a channel, not a person, and it must be saved so latecomers and other devices can read it. Where does it go, and who is it for?
Design decision: A message is sent to a channel (not a person) and latecomers must read it later. What happens to it?
The call: Persist to a per-channel store, then deliver. — The channel service looks up members and writes the message to ordered per-channel history first, so latecomers and other devices always get a complete record. Persist first, deliver second.
The Gateway hands the message to a Channel Service that looks up the channel’s members and writes the message to a per-channel Message Store. Persist first, deliver second — so history is always complete.
Channels are the unit: Modeling delivery around channels (with membership) rather than individual recipients is what makes group chat, joins and history natural. The message store is ordered per channel for clean scrollback.
Step 2 · Make it instant
WebSockets & sessions
Polling for new messages is laggy and wasteful. To feel live, the server must push messages the instant they arrive — but it needs to know which server holds each recipient’s connection.
Design decision: Messages must appear the instant they’re sent. How does the server reach the right client live?
The call: Persistent WebSockets + a session registry mapping user→server. — Each client holds a long-lived socket to a gateway; a registry records which server holds each user’s connection, so "deliver to user U" becomes "push on this socket on this server" instantly.
Clients hold a persistent WebSocket to a gateway. A Session Registry records which server each online user is connected to, so the system can route a message straight to the right connection and push it down immediately.
Stateful connections: Unlike stateless HTTP, chat gateways hold long-lived connections. The session registry is the directory that turns “deliver to user U” into “push on this socket on this server”.
Step 3 · Members are everywhere
Cross-server fan-out
A channel’s members are spread across many gateway servers. The server that received a message can only push to its own connected clients — everyone else would miss it.
Design decision: A channel’s members are spread across many gateway servers. The receiving server only knows its own clients. How does everyone get the message?
The call: Publish each message to a pub/sub Message Bus that all gateways subscribe to. — The sending gateway publishes once; every gateway subscribed to that channel pushes to its local sockets. One publish reaches members on any server, and gateways become interchangeable subscribers you can add freely.
Publish each accepted message to a Message Bus (pub/sub). Every gateway subscribes to the channels its connected users care about and pushes the message to those local sockets. One publish reaches members on any server.
Pub/sub bridges servers: The bus decouples “a message happened” from “who is connected where”. Gateways become interchangeable subscribers, so you can add servers without rethinking delivery.
Step 4 · Who’s around?
Presence & typing
The green dots and “Bob is typing…” are core to chat, but they’re extremely high-frequency and disposable — running them through the durable message path would swamp it.
Design decision: Green dots and "Bob is typing…" fire constantly and are disposable. How do you handle them?
The call: A separate ephemeral path: in-memory presence, broadcast, never persisted. — Track online/away/typing in fast ephemeral storage and broadcast over the same sockets, but never store it. If everyone disconnects, presence simply vanishes — and a typing storm can’t risk the message store.
Handle Presence on a separate channel: track online/away/typing in fast, ephemeral storage and broadcast updates over the same WebSockets, but never persist them. If everyone disconnects, presence simply disappears.
Ephemeral vs durable: Messages are sacred and stored; presence is throwaway and in-memory. Keeping them on separate paths means a storm of typing events never risks the message store.
Step 5 · Find old messages
Search
Teams accumulate millions of messages, and the value of chat is partly the searchable record. Scanning the message store for a keyword across years of history is far too slow.
Design decision: Teams accumulate millions of messages and need to find old ones by keyword. How?
The call: Build an inverted Search Index at write time, enforce permissions on read. — Index messages as they’re written so queries hit the index, not the raw store — and scope results to channels the user can access. Permissions must be part of the query, not an afterthought.
Index messages into a Search Index (inverted index) as they’re written, scoped by channel and permissions. Queries hit the index, not the raw store, and only return messages the user is allowed to see.
Index for retrieval: The same inverted-index idea behind web search applies here: build it at write time, enforce access on read. Permissions must be part of the query, not an afterthought.
Step 6 · Pull them back
Notifications & unread
People aren’t always watching. Mentions and DMs need to reach them via push, and every client must show accurate unread counts and badges — consistently across devices.
Design decision: Every device must show accurate unread counts and badges, consistently. How do you compute "unread"?
The call: Store a per-user, per-channel last-read cursor; unread = messages after it. — Unread is just "messages after my last-read marker" — cheap to compute, and syncing that one cursor across devices keeps every client in agreement automatically.
A Notifications service tracks each user’s last-read position per channel to compute unread counts, and sends push notifications for mentions/DMs to offline users. Read state syncs across a user’s devices via the gateway.
Read state per user: Unread is just “messages after my last-read marker”. Storing a per-user, per-channel cursor makes counts cheap and keeps every device in agreement.
Step 7 · Scale & big channels
The sharp edges
A huge company workspace, and an announcement channel with tens of thousands of members, both strain fan-out and connection counts. Threads add another dimension to ordering and delivery.
Shard by channel (and workspace) so a channel’s membership and messages live together, and run many stateless gateways for the connection load. For giant channels, fan out lazily (members pull on read) rather than pushing to everyone at once; model threads as messages linked to a parent.
Push small, pull huge: Just like a news feed’s celebrity problem: push to normal channels, but for massive ones let clients pull, so one announcement doesn’t trigger tens of thousands of simultaneous pushes.
You did it
You just designed Slack.
- Channel-centric messaging: persist to a per-channel store, then deliver.
- WebSockets plus a session registry give instant, routed push delivery.
- A pub/sub bus fans each message out to members across all servers.
- Presence and typing run on a separate, ephemeral, never-persisted path.
- A write-time search index (with permissions) makes history findable.
- Per-user last-read cursors drive unread counts and offline notifications.
- Shard by channel and pull-fan-out for huge channels to scale.