Forward-deployed / Learning zone
System Designa standalone module
Lesson 04

Real-time systems

TL;DR

Three systems where latency is the product. A chat system serves 50 million daily users through WebSocket connections to stateful chat servers. It uses a key-value store for message history and heartbeat-based presence detection. A search autocomplete system responds in under 100ms at 48,000 peak QPS. It pre-computes top-k results at every node of a trie data structure, rebuilt weekly in batch and sharded by prefix range. And a video streaming platform at YouTube scale (5 billion daily views) parallelizes upload and transcoding through a DAG-based processing pipeline. It serves popular content from CDN edge nodes, and streams via adaptive bitrate protocols (MPEG-DASH, HLS) that adjust quality to the viewer's bandwidth in real time.

🎯 For the technical PM

Why it matters — In real-time systems, latency is the feature. A chat message that takes 3 seconds to deliver feels broken. Autocomplete that takes 500ms is useless — the user has already finished typing. A video that buffers for 10 seconds loses the viewer. These systems require fundamentally different architecture from request-response services: persistent connections, stateful servers, and aggressive caching at every layer.

What it changes in your decisions — You define latency budgets per interaction (message delivery < 200ms, autocomplete < 100ms, video start < 2s). You size infrastructure for concurrent connections, not just QPS. You accept that stateful servers are harder to operate than stateless ones, and plan accordingly.

Ask your eng team — "What's the delivery latency at P99 for a 1:1 message, a group message to 500 people, and a presence update — and which of those degrades first under load?"

Risk if ignored — Chat that feels sluggish compared to competitors. Autocomplete that returns stale or irrelevant suggestions because the trie update cycle is too slow. Video that buffers on mobile because the CDN strategy doesn't account for the long tail of content.


Chat system

The product: real-time messaging for 50 million daily active users. 1:1 chats, group chats (up to 500 members), online presence indicators, and message history. The fundamental challenge: HTTP is request-response, but chat requires the server to push messages to the client the instant they arrive.

Connection strategy: polling, long polling, WebSocket

Three approaches to server-to-client communication, with very different resource profiles:

Polling — The client sends a request every few seconds asking "any new messages?" The server responds immediately with either new messages or an empty response.

Long polling — The client sends a request, and the server holds it open until a new message arrives (or a timeout, typically 30-60 seconds). When a message arrives, the server responds immediately and the client opens a new connection.

WebSocket — A persistent, full-duplex connection initiated by an HTTP upgrade handshake. Once established, both client and server can send messages at any time over the same connection.

WebSocket is the clear winner for chat. The initial connection cost is amortized over thousands of messages, and the per-message overhead drops from ~800 bytes (HTTP headers) to ~6 bytes (WebSocket frame).

Chat system - websocket architecture
Client A sender WS
persistent ⟶
WS Server 1 stateful conn
publish ⟶
Message Queue Redis pub/sub
subscribe ⟶
WS Server 2 stateful conn
push ⟶
Client B receiver WS
Zookeeper
service discovery: which WS server holds which user
KV Store
user_id → ws_server mapping for routing
Message DB
persistent storage with message_id sequence
Presence
online/offline via heartbeat + fan-out
1:1 Chat
A → WS server → lookup B's server in KV → route via MQ → B
If B offline → push notification service
Group Chat
A → WS server → fetch group members → MQ per recipient server
Small groups: copy to each · Large groups: fan-out on read

Architecture: stateless + stateful tiers

The chat system separates stateless services from stateful chat servers:

Stateless + stateful tiers

Chat servers hold connections; everything else is a normal HTTP service

Service discovery assigns each client to a chat server by load and proximity.

Client apps
↔ WebSocket
Chat Servers — stateful
Client apps
→ HTTP
Stateless Services — auth, profile, group mgmt
→
Relational DB
Chat Servers → KV Store — chat history
Service Discovery (Zookeeper) → assigns clients to Chat Servers

Stateless services — Authentication, user profile, group management, and all non-chat API calls. These are standard HTTP services behind a load balancer, horizontally scalable, using a relational database.

Stateful chat servers — Each client maintains a WebSocket connection to one chat server. The server tracks which clients are connected and routes messages accordingly. Because connections are stateful, you can't arbitrarily load-balance them. A client must reconnect to a specific server, or to any server if there's a shared presence layer.

Service discovery (Zookeeper) — When a client connects, the service discovery layer assigns it to a chat server based on server load, geographic proximity, and available capacity. Zookeeper maintains the registry of available chat servers and their current connection counts.

Chat storage: why not RDBMS

Message history has a distinctive access pattern:

A relational database struggles with this. Random reads across a 2-billion-row-per-day table are slow, and the write volume overwhelms single-master replication. A key-value store (HBase, Cassandra) is the standard choice:

The time-ordered key ensures that recent messages are physically co-located on disk, making the dominant query (latest messages) a fast sequential read.

Message flow: 1:1 chat

  1. User A sends a message via their WebSocket connection to Chat Server 1
  2. Chat Server 1 generates a message ID (time-ordered) and stores the message in the KV store
  3. Chat Server 1 checks the connection registry: is User B connected? To which server?
  4. If User B is connected to Chat Server 2, route the message to Chat Server 2 via an internal message queue
  5. Chat Server 2 pushes the message to User B over their WebSocket connection
  6. If User B is offline, the message is stored and a push notification is sent via the notification system

Message flow: group chat

Group messages use a per-recipient inbox model. When a message is sent to a group of 500 members:

  1. The message is stored once in the KV store
  2. A copy of the message reference (message ID + group ID) is written to each member's inbox
  3. Each member's chat server pushes the message to connected members
  4. Offline members receive it from their inbox when they reconnect

This is essentially fanout-on-write at the message level — similar to the news feed pattern. For small groups (under 500), the fanout cost is acceptable. For broadcast channels with millions of subscribers, you'd switch to fanout-on-read.

Message sync

When a client reconnects (after a network switch, app background/foreground, or server failover), it needs to catch up on missed messages. The client tracks cur_max_message_id — the ID of the last message it received. On reconnection:

  1. Client sends cur_max_message_id to the chat server
  2. Server queries the KV store: all messages in the user's channels with ID > cur_max_message_id
  3. Server pushes the delta to the client

This is efficient because message IDs are time-ordered — the query is a simple range scan.

Online presence

Presence (online/offline/away status) seems simple but is surprisingly expensive at scale. With 50M daily users, presence changes are frequent (phone locks, network switches, app backgrounding).

Heartbeat model:

  1. Client sends a heartbeat to the presence server every 5 seconds via the WebSocket connection
  2. If the server doesn't receive a heartbeat for 30 seconds, it marks the user as offline
  3. When a user's status changes, the presence server publishes the update

Fanout for presence updates: If User A has 500 friends online, each status change triggers 500 push updates. This is manageable for 1:1 friend lists. For group chats, presence is fetched on-demand (when a user opens the group info) rather than pushed in real time.

Online presence

Heartbeat every 5s · offline after 30s of silence

A status change fans out to every online friend's chat server.

User Client
heartbeat every 5s →
Presence Service
status change →
Pub/Sub Channel
Friend 1's chat server
Friend 2's chat server
Friend N's chat server

Search autocomplete

The product: as a user types in a search box, show the top 5 most relevant completions in under 100ms. With 10 million daily active users, each typing an average of 5 queries with 6 characters each, that's 300 million keystrokes per day — about 3,500 QPS average, 48,000 at peak (roughly 14x average, accounting for time-of-day concentration).

The trie data structure

A trie (prefix tree) is the natural data structure for prefix matching. Each node represents a character, and the path from root to a node represents a prefix. Searching for "din" walks root → d → i → n in O(L) time, where L is the prefix length.

The naive approach — walk to the prefix node, then enumerate all descendants to find completions — is too slow. A subtree might contain millions of terms. Instead, cache the top-k results at every node:

Trie with cached top-k

Query = one traversal, O(L) then O(1)

Every node caches its top-5 completions, rebuilt periodically.

(root)
↓
ddinner, disney, dinosaur, dine, direct
↓
didinner, disney, dinosaur, dine, direct
↓
dindinner, dinosaur, dine, dining, dingo
disdisney, discover, discount, discuss, display
↓
dinndinner, dinnerware, dinning...
dinodinosaur, dino, dinosaurs...

With top-k cached at each node, the query is a single trie traversal — O(L) to reach the prefix node, then O(1) to return the cached results. The tradeoff: updating cached results on every query is expensive, so the trie is rebuilt periodically.

Two-service architecture

The system splits into a Data Gathering Service (offline, batch) and a Query Service (online, real-time):

Two-service architecture

Offline batch build, online microsecond serve

Weekly rebuilds are intentional — established queries rarely change day to day.

Data Gathering Service — offline
Query Logs
→
Aggregator — weekly batch
→
Trie Builder
→
Trie Snapshot → object storage
Query Service — online
User keystroke
→
API Server
→
Browser/CDN cache
miss →
Trie Servers — loaded from snapshot

Data Gathering Service: Runs weekly (or more frequently for trending queries). Aggregates query logs, computes frequency-weighted rankings, builds a new trie with top-k cached at every node, and stores the trie snapshot in object storage (S3). The weekly cadence is intentional — daily rebuilds rarely change the results for established queries, and the batch processing cost is non-trivial. For trending/breaking topics, a separate real-time pipeline can inject hot queries into the trie between rebuilds.

Query Service: Trie servers load the latest snapshot into memory. On a user keystroke, the API server routes the prefix to the appropriate trie server, which returns the cached top-k in microseconds. If the trie is too large for a single server's memory, it's sharded.

Trie sharding

Two strategies for distributing the trie across multiple servers:

By prefix range — Server 1 handles a-f, Server 2 handles g-m, etc. Simple, but uneven — prefixes starting with 's' or 'c' have far more entries than 'x' or 'z'. Adjust ranges based on measured query distribution rather than alphabet position.

By hash — Hash the prefix and route to a server. Even distribution, but a single query can't be answered by a single server if prefixes of different lengths hash to different servers. Less practical for tries.

Prefix-range sharding with uneven splits (based on empirical query volume) is the standard approach.

Client-side optimization

The client plays a crucial role in keeping latency low and reducing server load:

These optimizations can reduce actual server QPS by 80%+ compared to naive per-keystroke requests.

Data freshness vs. cost

The weekly batch rebuild means truly trending queries (breaking news, viral events) won't appear in autocomplete for up to a week. Options:


YouTube / video streaming platform

The product: a video platform at YouTube scale — 5 billion videos watched per day, hundreds of hours of video uploaded every minute. The system must handle video upload, processing (transcoding into multiple formats and resolutions), storage, and streaming — each with distinct scaling challenges.

Upload flow: parallel processing

Video upload is split into two parallel streams that proceed independently:

Upload flow: parallel processing

Metadata is live in seconds; video processing is the slow part

The two streams proceed independently.

Creator uploads → Upload Service
Metadata path — fast
Metadata Service
Metadata DB — title, description, tags
Metadata Cache
Video path — slow, compute-intensive
Video Storage — original file
Transcoding Pipeline
Transcoded Storage → CDN Distribution
  1. Video file — uploaded to a temporary storage location, then passed to the transcoding pipeline
  2. Metadata — title, description, tags, thumbnail are stored in a relational database and cache immediately

The metadata path completes quickly (the video page can be "live" with a "processing" status). The video processing path is the slow, compute-intensive part.

Transcoding: DAG-based pipeline

Video transcoding (converting a raw upload into multiple formats, resolutions, and bitrates) is not a single operation — it's a directed acyclic graph (DAG) of dependent tasks:

Transcoding: DAG-based pipeline

Video and audio encode in parallel, then merge

GOPs (Group of Pictures) enable each resolution to encode independently.

Original video
↓ Preprocessor — split into GOPs
360p H.264
720p H.264
1080p H.264
4K H.265
Audio AAC
Audio Opus
↓ Muxer — combine audio + video
Thumbnail generation
Watermark / DRM
Quality check → Transcoded storage

The pipeline has four components:

  1. Preprocessor — Validates the video, splits it into GOPs (Group of Pictures — small, independently decodable segments). GOPs enable parallel encoding.
  2. DAG Scheduler — Determines task dependencies and parallelism. Video encoding at different resolutions can run in parallel. Audio and video can be processed independently. Muxing depends on both completing.
  3. Resource Manager — Allocates CPU/GPU workers to tasks. GPU-intensive tasks (H.265 encoding, 4K) get GPU workers. Lighter tasks (thumbnail generation) get CPU workers.
  4. Task Workers — Execute individual encoding tasks. Horizontally scaled, stateless — they pull tasks from a queue, process the GOP, and write the output to storage.

Streaming: adaptive bitrate

Users watch videos on devices ranging from 4K TVs on fiber to phones on 3G. Serving a single bitrate is wasteful (too high for slow connections, too low for fast ones). Adaptive bitrate streaming solves this:

The video is pre-encoded at multiple quality levels. Each quality level is split into small segments (2-10 seconds). The player downloads segments one at a time, choosing the quality level that matches its current bandwidth. If bandwidth drops (the viewer enters a tunnel), the player switches to a lower quality for the next segment — seamlessly, without rebuffering.

Two dominant protocols:

Protocol Developed by Container Adoption
MPEG-DASH MPEG consortium MP4 Open standard; Netflix, YouTube
HLS (HTTP Live Streaming) Apple MPEG-TS or fMP4 Required on iOS/Safari; widely supported

Both work the same way: a manifest file lists available quality levels and segment URLs. The player downloads the manifest, then fetches segments at the appropriate quality. All delivery happens over standard HTTP/HTTPS — no special streaming servers needed.

Video storage and delivery have a distinctive distribution: a small percentage of videos account for the vast majority of views (head), while millions of videos are watched rarely (long tail).

CDN strategy: popular vs. long-tail

A small share of videos account for most views

A viral video absorbs >99% of its traffic at the CDN edge.

Popular content — head
CDN Edge Servers, worldwide

Viewer → popular video → CDN

Long-tail content
Origin Servers — centralized storage

Viewer → rare video → Origin

Origin → on first request → CDN, promoting a suddenly-viral video from origin to edge automatically

Popular videos are pushed to CDN edge servers worldwide. The CDN caches them close to viewers, reducing latency and origin load. For a viral video, the CDN absorbs >99% of the traffic.

Long-tail videos stay on origin servers. Serving them from CDN would be wasteful — the CDN storage cost exceeds the benefit for a video watched once a month. If a long-tail video suddenly becomes popular (someone shares it on social media), the CDN pulls it on the first request and caches it, transitioning it from origin to edge automatically.

Upload security: pre-signed URLs

Users upload videos directly to object storage (S3), bypassing the application servers. This avoids using application-server bandwidth for large file transfers. The flow:

  1. Client requests an upload URL from the API server
  2. API server generates a pre-signed URL — a time-limited, one-use URL that grants write access to a specific storage path
  3. Client uploads the video directly to storage using the pre-signed URL
  4. Storage notifies the transcoding pipeline via an event (S3 event notification, or a message queue)

Pre-signed URLs are secure: they expire (typically in 15-60 minutes), they're scoped to a specific path, and they can't be reused. The client never receives storage credentials.

Cost optimization

Video infrastructure is expensive. Key optimizations:


Failure modes

Practitioner checklist