Complex Systems · Phase 4: High-Level Design (HLD)
Global Real-Time Chat & Messaging (Discord & WhatsApp)
Build a planetary messaging system using a 50M-concurrent-connection worked example with persistent WebSocket gateways, scoped ordering, distributed presence, durable logs, and delivery receipts.
Primary specification: How Discord Stores Billions of Messages (Discord Engineering)
Lesson 28 of 33
1d streakThe Architectural Question
Broadcasting presence updates (Online/Offline) for a user in a Discord guild with 500,000 members creates 500,000 simultaneous websocket messages, crashing connection nodes.
Core Engineering Objectives
- Stateful connection management: WebSocket gateway clusters with Redis/ZooKeeper session routing tables
- Distributed user presence: heartbeat leases, fanout batching, and presence aggregation
- Append-only message storage: ScyllaDB / Cassandra wide-column clustering for high-write chat logs
Step 1: Problem Statement & Operational Context
1. Sender
Sends a message with a client-generated ID.
2. Gateway
Authenticates the long-lived connection.
3. Chat service
Validates membership and deduplicates retries.
4. Message log
Durably orders messages within a conversation.
5. Recipient gateway
Pushes online delivery; offline users sync later.
A delivery acknowledgement is not the same as a read receipt; model those states separately.
Real-time chat platforms power instant communication across 1-on-1 direct messages (DMs), private groups, and large community channels. This worked example assumes 50 million concurrent connections. It targets low delivery latency, a deterministic order within each conversation, synchronized delivery/read receipts, live presence, and durable append-only message persistence. These are design targets for the exercise, not claims about one named product.
The Stateful Gateway Scalability Challenge
Stateful WebSocket Gateway
An edge server that terminates client TCP handshakes, negotiates TLS, maintains long-lived bidirectional WebSocket sockets, and serializes/deserializes lightweight binary frames.
Session Registry
A high-throughput distributed registry (Redis Cluster or distributed hash table) mapping each online `user_id` to their active `gateway_node_id` and socket descriptor.
LSM-Tree Wide-Column Store
A storage engine (Apache Cassandra / ScyllaDB) optimized for sequential append-only writes, partitioned by channel ID and clustered by monotonically increasing message IDs.
Step 2: Requirements (Functional & Non-Functional)
The communication engine must satisfy uncompromising real-time SLAs while supporting diverse conversation topologies.
| Category | Requirement Specification | Architectural Implementation |
|---|---|---|
| Functional: 1-on-1 & Group Chat | Instant bidirectional messaging in DMs and channels up to 100k users | Stateful WebSocket gateways + distributed Pub/Sub message routing bus |
| Functional: Message History | Paginate past chat history seamlessly across all devices | ScyllaDB wide-column store clustered by message_id DESC |
| Functional: Presence Tracking | Show live status (Online, Idle, Do Not Disturb, Offline) | 30s heartbeat leases in Redis with lazy viewport fanout |
| Functional: Receipts & Read State | Track Sent, Delivered, and Read status markers | Atomic channel sequence watermarks (last_read_message_id) |
| Functional: Offline Delivery | Deliver push notifications to disconnected recipients | Kafka consumer worker triggering Apple APNs and Google FCM |
| Non-Functional: Latency | End-to-end message delivery latency < 100ms at p99 | Epoll/kqueue network I/O + in-memory message routing |
| Non-Functional: Concurrency | 50,000,000 concurrent persistent WebSocket connections | Fleet of ~500–1,000 gateway instances running lightweight actor processes |
| Non-Functional: Message Durability | Zero message loss once acknowledgment is returned to sender | Write-Ahead Logging (WAL) and quorum persistence in distributed storage |
Use it when
Adopt an LSM-tree wide-column database (ScyllaDB / Cassandra) for chat history when write volume exceeds 50,000 msgs/sec and queries strictly fetch chronological slices for a specific channel ID.
Reconsider when
Use a relational database (PostgreSQL) when channel membership is small (<1,000 users) and enterprise compliance requires cross-channel relational joins and complex text searching.
Step 3: Detailed Back-of-the-Envelope Capacity Estimations
Capacity planning sizes the socket memory footprint, network bandwidth, and five-year storage tier for 50M concurrent connections.
| System Dimension | Calculation / Formula | Sustained Sizing | Peak Load Headroom |
|---|---|---|---|
| Concurrent Connections | Given design scope | 50,000,000 WebSockets | 65,000,000 WebSockets |
| Memory Per Idle Socket | Tuned Linux rmem/wmem buffers + TLS context | ~10 Kilobytes | ~16 KB per socket |
| Total Gateway Cluster RAM | 50,000,000 * 10 KB | ~500 Gigabytes RAM | ~1.0 TB provisioned across fleet |
| Gateway Nodes Required | 50M sockets / 50,000 sockets per node (32GB RAM) | ~1,000 gateway nodes | ~1,200 nodes for N+2 redundancy |
| Daily Message Volume | 1,000,000,000 messages / 86,400s | ~11,574 messages/sec | ~50,000 messages/sec |
| Presence Heartbeats | 50,000,000 users / 30-second heartbeat interval | ~1,666,666 heartbeats/sec | ~3,000,000 heartbeats/sec |
| Average Message Size | Message ID (8B) + Sender (16B) + Channel (16B) + Text (150B) | ~250 bytes | ~500 bytes with attachments |
| 5-Year Message Storage | 1 Billion msgs/day * 365 days * 5 yrs * 250B | ~456.25 Terabytes | ~1.37 Petabytes (3x replication) |
| Duplex Network Bandwidth | 11,574 msgs/s * 5 average fanout * 250 bytes | ~14.46 Megabytes/sec | ~75 MB/sec (~600 Mbps) |
The Enormous Scale of Presence Heartbeats
Step 4: High-Level Architecture & Component Responsibilities
The architecture decouples stateful connection management from business logic, message routing, and persistent storage.
- 01Layer 4 TCP Load Balancers (AWS NLB / Maglev): Distributes incoming TCP/TLS handshakes across the gateway fleet using 5-tuple hashing with Direct Server Return (DSR).
- 02Stateful WebSocket Gateways (Erlang/Elixir BEAM or Go): Maintains open TCP sockets, serializes JSON/Protobuf frames, validates user tokens, and runs local connection heartbeats.
- 03Session Registry Cluster (Redis Cluster): Maintains a lightning-fast distributed mapping:
user_id -> gateway_instance_id. Keys are kept alive via 60-second TTLs refreshed by gateway heartbeats. - 04Message Routing Engine (NATS / RabbitMQ / Redis Pub/Sub): Routes messages from the author's gateway to the recipient's gateway node.
- 05Message Persistence Workers (Kafka Consumer): Consume durably accepted messages and batch writes to ScyllaDB. Whether delivery waits for database persistence is an explicit latency-versus-durability decision.
- 06ScyllaDB / Cassandra Cluster: Durable wide-column storage for channel message histories, partitioned by channel ID and time buckets.
- 07Push Notification Worker Fleet: Dispatches Apple APNs and Google FCM alerts when the session registry indicates that a recipient is offline.
Why Stateful Gateways Require Custom Drain Mechanics
Step 5: Detailed Data Models & Storage Schema
The persistence layer uses ScyllaDB (or Apache Cassandra) CQL schemas designed around immutable, append-only wide-column rows.
-- ScyllaDB / Apache Cassandra Message Storage Schema
CREATE KEYSPACE chat_system WITH replication = {
'class': 'NetworkTopologyStrategy',
'us-east': 3,
'eu-west': 3
};
-- Time-bucketed message schema prevents unbounded partition growth
CREATE TABLE chat_system.messages (
channel_id UUID,
bucket_id INT, -- Time bucket: YYYYMM (e.g., 202609)
message_id BIGINT, -- Monotonic Snowflake ID
sender_id UUID,
content TEXT,
attachments JSON, -- Image/video metadata URLs
is_pinned BOOLEAN,
created_at TIMESTAMP,
PRIMARY KEY ((channel_id, bucket_id), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);
-- Channel Read Receipts and Watermarks
CREATE TABLE chat_system.channel_read_markers (
channel_id UUID,
user_id UUID,
last_read_message_id BIGINT,
last_read_at TIMESTAMP,
PRIMARY KEY (channel_id, user_id)
);
-- Redis Session Registry Key Schema:
-- Key: session:{user_id} -> Value: "gateway-node-88" (TTL: 60s)
-- Key: presence:{user_id} -> Value: "ONLINE" (TTL: 35s)Architectural Boundary · Do Not Confuse
ScyllaDB / Cassandra Wide-Column LSM
MongoDB / Document Store
Embedding every message in one growing MongoDB conversation document eventually encounters document-size and write-contention limits. A wide-column model stores each message as a row and clusters rows for bounded conversation-range reads. It scales by partitioning, but it is not infinite and range-read cost grows with the rows and partitions scanned.
Step 6: Core Algorithms & Step-by-Step Request Flows
The end-to-end message delivery flow coordinates socket gateways, session registries, and message brokers with sub-100ms latency.
// Production Gateway Message Dispatcher & Routing Logic
export interface ChatMessageFrame {
channelId: string;
messageId: bigint;
senderId: string;
recipientId?: string; // Present in 1-on-1 DM
content: string;
timestamp: number;
}
export class GatewayMessageRouter {
constructor(
private sessionRegistry: RedisClient,
private messageBus: PubSubBroker,
private localSocketManager: SocketManager
) {}
async handleInboundMessage(frame: ChatMessageFrame): Promise<void> {
// 1. Assign monotonic Snowflake message ID if not present
frame.messageId = SnowflakeGenerator.nextId();
frame.timestamp = Date.now();
// 2. Publish to Kafka for asynchronous durable persistence (WAL)
await kafkaProducer.send('chat-messages-persistence', frame);
// 3. 1-on-1 Direct Message Route
if (frame.recipientId) {
const targetGateway = await this.sessionRegistry.get(`session:${frame.recipientId}`);
if (!targetGateway) {
// Recipient is OFFLINE: Route to Push Notification Queue
await kafkaProducer.send('offline-push-notifications', {
userId: frame.recipientId,
title: `New message from ${frame.senderId}`,
body: frame.content,
});
return;
}
if (this.localSocketManager.hasLocalSocket(frame.recipientId)) {
// Recipient is connected to THIS SAME gateway instance! Direct write:
this.localSocketManager.sendFrame(frame.recipientId, frame);
} else {
// Recipient is on a different gateway: Dispatch via Pub/Sub topic for that node
await this.messageBus.publish(`gateway-inbox:${targetGateway}`, frame);
}
}
}
}- 01Step 1: Socket Ingress: Alice sends a chat frame over her WebSocket to Gateway Node 12.
- 02Step 2: Conversation Sequencing: A partition or sequencer keyed by
channel_idassigns a monotonically increasing channel sequence. A Snowflake-style ID may remain the globally unique identifier, but it is not used as a perfect global clock. - 03Step 3: Persistence Queue: Message is emitted to Kafka topic
chat-messages. A consumer worker persists the row into ScyllaDB with quorum consistency (LOCAL_QUORUM). - 04Step 4: Recipient Discovery: Router queries Redis Session Registry for Bob's active gateway:
GET session:bob_uuid. - 05Step 5a (Bob is Online): Registry returns
"gateway-node-88". Router publishes the frame to NATS topicgateway.node88. Gateway Node 88 consumes the frame and pushes it down Bob's open WebSocket socket in <15ms. - 06Step 5b (Bob is Offline): Registry returns
null. Router pushes payload to Apple APNs / Google FCM queue to wake up Bob's phone. - 07Step 6: Delivery Acknowledgment: Bob's client receives frame and automatically emits an ack frame
DELIVERED. Gateway updates Alice's UI with a double-check marker.
Step 7: Deep Dives into Critical Bottlenecks
The 100,000-Member Guild Presence Blast (O(N^2) Collapse)
Cassandra Partition Size Degradation & Time Bucketing
channel_id partition grows with the lifetime of a conversation, increasing compaction, repair, streaming, and read costs. Cassandra and ScyllaDB do not provide one universal safe partition limit for every schema and workload. Choose a target from measured row sizes and operational tests, then bucket by ((channel_id, bucket_id), message_id). Busy channels may need daily buckets while quiet channels can use monthly buckets.Step 8: Real-World Production War Stories
Discord: Migrating from MongoDB to Cassandra, and Cassandra to ScyllaDB
WhatsApp: 2 Million Concurrent Connections Per FreeBSD Node
maxfiles, kern.ipc.somaxconn), shrinking TCP send/receive buffer windows to 4KB for idle connections, and optimizing Erlang BEAM process heap allocations, WhatsApp sustained over 2.1 million concurrent connected TCP sockets on a single physical commodity server with zero degradation.Step 9: Interviewer Deep-Dive Gauntlet & Active Recall
Deep-Dive Q1: How do you guarantee strict total message ordering in a distributed channel when multiple users post concurrently through different gateway nodes?
Deep-Dive Q2: How do you implement unread message counters without executing expensive `COUNT(*)` database queries on every login?
SELECT COUNT(*) FROM messages WHERE channel_id = ? AND message_id > last_read_id across 500 channels for 50 million users would instantly melt any database cluster. The production solution tracks watermarks: 1) Every channel maintains an authoritative atomic sequence counter latest_message_id. 2) The user record maintains last_read_message_id. 3) Maintain an integer unread counter in Redis: HINCRBY user_unreads:{user_id} {channel_id} 1 when a new message is published. 4) When the user opens the channel, update last_read_message_id = latest_message_id and reset the Redis counter to 0 with HDEL. Zero disk queries are executed.Deep-Dive Q3: How do you handle mobile client network transitions (e.g. WiFi to 5G cellular) without dropping messages or duplicating posts?
dedup_id). 2) Heartbeat Deadman Switch: Gateways send WebSocket ping/pong frames every 30 seconds; if no response within 10 seconds, the server terminates the dead socket and evicts the session registry. 3) Reconnection Re-sync: Upon reconnecting, the client sends a SYNC frame containing last_received_message_id. The server queries ScyllaDB for any messages where message_id > last_received_message_id and streams the missing delta. 4) Idempotent Ingestion: If a client re-sends a message due to a dropped ACK, the server deduplicates against the dedup_id index in Redis.Deep-Dive Q4: Why is Erlang/Elixir BEAM or Go preferred over traditional thread-per-connection architectures (like standard Java Tomcat) for chat gateways?
Active Recall Checkpoint
Calculate the aggregate memory required across all gateway nodes to maintain 50 million idle TCP WebSocket connections at 10 KB per connection.
Hint: Multiply 50,000,000 by 10 Kilobytes and convert to Gigabytes.
Show answer & explanation
50,000,000 * 10 KB = 500,000,000 KB ≈ 500,000 MB = 500 Gigabytes of RAM. Distributed across 50 gateway nodes (with 16GB RAM each) or 1,000 smaller container pods, this memory footprint is easily manageable in modern cloud infrastructure.
Active Recall Checkpoint
Why should large channels (e.g., 50,000 members) avoid broadcasting user presence state changes to all channel members?
Hint: Consider the quadratic O(N^2) write amplification of status updates.
Show answer & explanation
Broadcasting every status change to 50,000 members generates 50,000 socket frames per event. If 1,000 members change status in a short window, that single room produces 50 million frames, overflowing socket buffers. Production systems use Viewport Presence, sending status updates only for the members currently visible on the active client screen.
Topic sizing snapshot
The four numbers that materially affect this architecture. Use them as bounds, not as a generic calculator embedded in every lesson.
Peak QPS
50,000 Messages/sec peak (exercise assumption)
5-Yr Storage
~456 TB raw; ~1.37 PB at 3x replication
Bandwidth
~0.6 Gbps at the stated average fanout; test peak fanout separately
Working Set RAM
~500 GB at 10 KB per idle connection; provision ~1 TB with headroom
Production Postmortem & Verification
Production Incident Postmortem
- Trigger / Setup: A popular gamer comes online in a 250,000-member Discord server configured for real-time presence broadcast.
- Production Symptom: Gateway nodes attempt to write 250,000 socket frames simultaneously; socket buffers overflow and drop client connections.
- Root Cause & Fix: Presence in large channels must not be broadcast individually; use lazy pull, viewport-only presence, or bucketed aggregations.
Acceptance Criteria & Outcomes
- Size socket connection memory for 50M concurrent WebSocket connections across gateway clusters.
- Model chat message schema in Cassandra/ScyllaDB: `channel_id (Partition Key)`, `message_id (Clustering Key DESC)`.
- Implement delivery receipt state transitions: Sent (single tick) -> Delivered (double tick) -> Read (blue tick).
End-of-Chapter Synthesis & Key Takeaway
Global Real-Time Chat & Messaging (Discord & WhatsApp) is built around one fundamental rule: Establish clear responsibility and explicit trade-offs.. Before adding components, establish explicit quantitative SLAs for latency, throughput, and consistency. Choose the simplest architecture that meets stated constraints.
Architectural & Technical Interview Review
Core FAANG Interview Questions
Q01How do you justify introducing Global Real-Time Chat & Messaging (Discord & WhatsApp) into a high-scale production architecture?Click to reveal model answer →Hide answer ↑Requirements & Design Brief
Introduce Global Real-Time Chat & Messaging (Discord & WhatsApp) only when measured scale, data size, or failure boundaries exceed single-node capacity. State the exact bottleneck, evaluate operational overhead, and verify that the trade-offs justify the added moving parts.
Q02What is the primary trade-off and hidden operational cost of Global Real-Time Chat & Messaging (Discord & WhatsApp)?Click to reveal model answer →Hide answer ↑Architecture Trade-offs
It improves scalability and availability, but introduces network hops, serialization overhead, and eventual consistency or coordination complexity.
Q03How do you handle catastrophic failure or dependency degradation in Global Real-Time Chat & Messaging (Discord & WhatsApp)?Click to reveal model answer →Hide answer ↑Failure Review & Recovery
Implement connect/read timeouts, exponential backoff with jitter, and fallback defaults so downstream degradations do not cause cascading outages.
Production Pitfalls & Anti-Patterns
Critical Failure Anti-Patterns
Premature Distributed Complexity
Scattering distributed components before identifying concrete write, read, or storage limits creates accidental points of failure and operational debt.
Production Mitigation: Start with the simplest modular design that satisfies requirements, evolving it only as measured constraints dictate.
Unbounded Retries and Cascading Storms
When a dependency degrades, immediate synchronized retries multiply downstream traffic, turning a minor slowdown into a total fleet outage.
Production Mitigation: Enforce strict retry budgets, exponential backoff, and full randomized jitter on all client calls.
Neglecting Observability and Metrics
Deploying architecture without distributed tracing or p99 tail latency alerts leaves engineers blind when partial partitions occur.
Production Mitigation: Export metrics for request rate, error rate, p95/p99 duration, and saturation at every service boundary.