Build Slack's Channel Message Fan-Out and Unread Counts


distributed-systems scalability performance

Slack Message Fan-Out System

Delivering one message to thousands of channel members in real-time, while keeping unread counts accurate across every device you own.

14 min read Advanced Blueprint

You are typing in a Slack channel with 8,000 members. You hit Enter. Within 150 milliseconds, every online member’s screen shows a new message. Within the same window, their unread badge increments by exactly one. Open Slack on your phone - same badge count. Close the channel on desktop, open it on your iPad - the badge clears. The server knows you read it.

This is not magic. It is a precisely engineered fan-out pipeline that touches half a dozen distributed systems in under two hundred milliseconds. The deceptive simplicity of the Slack UI hides one of the hardest problems in real-time systems: how do you push a single event to thousands of subscribers without either crushing your write throughput or leaving users staring at stale data?

Think of it like a radio broadcast tower. One station transmits. Ten million radios receive. The trick is that every radio has a different signal strength, some channels have 5 members and some have 50,000, and you also need to track which stations each listener has tuned into and for how long. That is the Slack fan-out problem in one sentence.

Three decisions define the entire architecture. First, how do you store and look up the list of channel members efficiently for channels that range from 2 to 500,000 users? Second, how do you fan out a message to all of them within your latency budget without blowing up your write amplification? And third, how do you maintain per-user per-channel unread counts that stay accurate even when users open the same channel on three devices simultaneously?

Requirements and Constraints

Functional Requirements

  • Send a message to a channel and deliver it in real-time to all online members
  • Maintain accurate unread message counts per user per channel
  • Track a read cursor (last-read position) per user per channel
  • Sync read state across all devices a user has logged in on
  • Support channels from 2 to 500,000 members (enterprise grid workspaces)
  • Handle edit and delete events propagating to all members
  • Push notifications to offline users via APNs / FCM

Non-Functional Requirements

Metric Target
Message delivery p99 latency < 200ms for online users
Unread count accuracy Eventual, within 1 second
Cross-device sync latency < 500ms
Fan-out write throughput 500K messages/second peak
WebSocket connections 10M concurrent (system-wide)
Channel member list reads < 5ms at p99
Unread counter reads < 2ms at p99
System availability 99.99%

Traffic Estimates

Slack processes roughly 1.5 billion messages per day. At peak, that is ~20,000 messages per second. With an average of 50 channel members, that is 1 million fan-out write operations per second at peak. Counter increments run at 2-5x the message rate due to multi-device registrations.

Key Insight
The dominant cost is not the message store write. It is the fan-out write amplification: one message times N members. A channel with 10,000 members receiving 10 messages per minute generates 100,000 write events per minute from a single channel alone. Multiply by thousands of active channels and you understand why this is a hard problem.

High-Level Architecture

The system decomposes into six primary components: a WebSocket Gateway, a Message Service, a Fan-Out Engine backed by Kafka, a Channel Member Store, a Redis-based Unread Counter service, and a Read Cursor Store.

High-level architecture showing client layer, WebSocket gateway, Message Service, Fan-Out Engine, Kafka bus, and storage layer

The data flow for a sent message proceeds as follows. The sender’s client sends a chat.postMessage payload over the persistent WebSocket connection. The Gateway authenticates the request, verifies channel membership, and forwards to the Message Service. The Message Service assigns a monotonic timestamp-based ID (Slack calls this msg_ts), persists the message to the message store, and publishes a channel_message event to a Kafka topic partitioned by channel_id. Fan-Out Workers consume from Kafka, look up the channel member list from Redis, and in parallel push to online WebSocket connections and increment Redis unread counters for all members. Offline users receive mobile push notifications via APNs/FCM.

Key Insight
The Kafka bus is the decoupling point that makes this work. The Message Service writes once. Fan-Out Workers consume asynchronously. If the fan-out layer falls behind, messages still persist - workers catch up. You can also add new consumers (analytics, search indexing, audit logs) without touching the hot write path.

Channel Subscriber Management

Every channel maintains a member list. Querying this list on every message send is the hottest read in the system - it must be fast.

Storage Strategy

For channels up to 10,000 members, the member list lives as a Redis Set using the key channel:members:{channel_id}. The value is a set of user_id strings. SMEMBERS returns all members in O(N) but N is bounded and the operation completes in microseconds for typical channel sizes.

# Add member to channel
SADD channel:members:C012345 U001 U002 U003

# Get all members for fan-out
SMEMBERS channel:members:C012345

# Check membership (used at message send to verify auth)
SISMEMBER channel:members:C012345 U001

# Get member count
SCARD channel:members:C012345

For large channels (10,000+ members), SMEMBERS returning all user IDs in one shot becomes expensive both in memory and network transfer. The system switches to a paginated approach using Redis Sorted Sets, where the score is the member’s join timestamp. The Fan-Out Worker iterates using ZSCAN in batches of 1,000.

# Large channel: sorted set keyed by join time
ZADD channel:members:large:C099999 1717200000 U001
ZADD channel:members:large:C099999 1717200001 U002

# Paginated scan for fan-out workers
ZSCAN channel:members:large:C099999 0 COUNT 1000

Premium Content

Unlock the full article along with everything else in the archive — all in one place.

In-depth analysis Expert insights Full archive access
Unlock Full Article