Skip to content

02. Live Streaming Platform — Low-Level Design

~18 min read · Part 3 of 4 (Overview → HLD → LLD → Q&A)

Data models

CREATE TABLE stream (
    channel_id   BIGINT PRIMARY KEY,
    stream_key   TEXT,                  -- broadcaster secret for ingest auth
    status       SMALLINT,              -- live / offline
    started_at   TIMESTAMP,
    ingest_node  TEXT                   -- current ingest server
);

-- Live manifest (rolling, in edge/cache):
--   /live/{channel}/{rendition}/index.m3u8  = last K segments only
--   #EXT-X-MEDIA-SEQUENCE grows; old segments fall out of the window
-- Chat room (transient):  room:{channel} pub/sub topic; optional ring buffer of last N msgs
-- Viewer count (approx):  hll:{channel} = HyperLogLog / sampled counter

The live manifest is a sliding window — it lists only the last K segments (#EXT-X-MEDIA-SEQUENCE advances, old segments drop). That's the core difference from VOD's complete manifest: a live player always requests near the live edge, so only recent segments need to exist at the edge.

Component internals

Real-time segmenter — racing the clock

def on_encoded_frames(channel, rendition, frames):
    buffer.append(frames)
    if buffer.duration() >= SEG_SEC:          # ~2s target
        seg = package(buffer.flush())
        n = seq.next(channel, rendition)
        blob.put_live(channel, rendition, n, seg)
        cdn.push_to_edges(channel, rendition, n, seg)   # warm as produced
        manifest.append_and_roll(channel, rendition, n, window=K)

The whole pipeline is a race against wall-clock: segment N must be produced, published, and pushed to edges in less time than it takes viewers to drain their buffer. Shorter SEG_SEC lowers latency but multiplies per-segment overhead (more requests, more manifest churn) — the central live tuning knob.

The latency fork — chunked HLS vs WebRTC

Chunked HLS/LL-HLS  (this design):
  latency ≈ segment_dur + player_buffer  → ~2–6 s
  scales to millions via ordinary CDN caching of segments
  best for: large audiences, one-to-many, non-interactive

WebRTC:
  latency < 500 ms (peer/SFU real-time transport)
  needs an SFU (selective forwarding unit) fleet, not plain CDN caching
  best for: interactive (auctions, betting, video calls), smaller fan-out

The choice is a genuine tradeoff, not a default. For the 5M-viewer esports final, chunked HLS is correct: a few seconds of latency is fine and CDN caching is what makes 20 Tbps affordable. For a sub-second interactive stream (a live auction), WebRTC via an SFU is worth its far higher per-viewer cost. Naming why you pick one is the senior move.

Chat fan-out — reduce, don't brute-force

def on_chat_message(channel, user, text):
    if not rate_limiter.allow(user, channel):     # slow mode / per-user cap
        return reject(user)
    msg = {user, text, ts}
    room_pubsub.publish(channel, msg)             # → all gateway shards for channel

def gateway_deliver(channel, msg):
    conns = local_conns[channel]
    if len(conns) > SAMPLE_THRESHOLD:             # huge room
        if not sample(msg): return                # show a legible subset
    for c in conns: c.send(msg)

The delivery product (50k msgs/s × 5M viewers) is physically impossible and pointless — no human reads 50k/s. So chat reduces fan-out: per-user rate limits cut the input, and on very large channels the gateway samples/batches what it forwards so each viewer sees a readable stream. Correctness for chat means "legible and timely," not "every message to everyone."

Core algorithm — approximate concurrent viewer count

join(channel, viewer_id):
    hll[channel].add(viewer_id)         # HyperLogLog: O(1), tiny memory, ~2% error
viewer_count(channel):
    return hll[channel].estimate()      # good enough for a live number

Millions joining at stream start would crush an exact counter. A HyperLogLog gives a viewer count within a couple percent using kilobytes — the right tradeoff, since "5.0M vs 5.02M viewers" is a display detail, not a decision input.

Sequence diagram — live segment race + chat

Broadcaster  Ingest   Transcode  Packager    Edge/CDN    Viewer     Chat gw   PubSub
  │ RTMP push │          │           │            │          │          │        │
  ├───────────▶│ frames ─▶│ encode ──▶│ seg N ────▶│ warm     │          │        │
  │           │          │           │            │◀ GET segN │          │        │
  │           │          │           │            │── segN ──▶│ (plays)  │        │
  │           │          │           │            │          │ "GG!" ──▶ │ publish▶│
  │           │          │           │            │          │◀── msg ── │◀───────┤ fan to shards

Concurrency and edge cases

  • Encoder falling behind: shed the top rendition and/or lengthen segments briefly to regain real-time; never let the live edge drift unbounded.
  • Broadcaster reconnect: ingest holds a short buffer and resumes the same #EXT-X-MEDIA-SEQUENCE so viewers see a brief stall, not a broken stream.
  • Late chat joiner: an optional ring buffer of the last N messages gives context on join without replaying the whole room.
  • Duplicate chat delivery across shards: dedup by message id at the gateway before sending to a connection.
  • DVR seek during live: seeking back reads retained segments (VOD store); returning to live jumps to the manifest's newest sequence.
  • Segment window eviction: a viewer whose buffer stalls past the window falls off the live edge and must re-join at the current sequence (can't request an evicted segment).
  • Stream end → VOD: on status=offline, retained segments are sealed into a complete manifest served by the OTT path.