Back to Notes

Design YouTube / Netflix (Video Streaming)

1. Problem & Real-World Context

Design a video platform: upload, process, and stream video at global scale with adaptive quality. The defining challenge isn't storage (that's [[Blob & Object Storage]]) or even streaming itself — it's that upload and playback are fundamentally different problems with different latency budgets: upload can be slow (users tolerate a video "processing" for minutes), playback cannot (buffering is the single biggest driver of viewer drop-off). The whole architecture exists to keep those two paths from coupling to each other's constraints.

2. Requirements

Functional: upload video; stream video with adaptive quality based on the viewer's bandwidth; search videos by title/tags; (recommendations explicitly out of scope unless the interviewer asks — naming the scope cut is itself a good signal).

Non-functional: high availability (99.99%+); low-latency streaming globally (viewers are everywhere, origin storage is not); eventual consistency acceptable for view counts (a delayed count is invisible to the user); durable storage — videos persist indefinitely (YouTube) or for a license period (Netflix).

3. Capacity Estimation

  • Upload volume: ~500 hours of video uploaded per minute (a real YouTube-scale figure) ≈ tens of thousands of new videos/minute — this alone tells you transcoding cannot be synchronous with upload (§5).
  • Storage per video: 1 hour of original video ≈ ~1GB; transcoded into ~5 quality tiers (360p–4K) ≈ ~5GB total per video stored. At 1M videos, that's ~5PB — squarely a [[Blob & Object Storage]] problem, not a filesystem-on-a-server problem.
  • Read:write ratio: heavily read-skewed, often cited around 1000:1 — a single popular video is watched millions of times against one upload, which is why nearly every design decision here (CDN caching, pre-transcoding all quality tiers upfront rather than on-demand) optimizes the read/playback path aggressively, even at real cost on the write/upload side.

4. High-Level Architecture

flowchart LR
    subgraph Upload
        C1[Client] --> AG[API Gateway] --> US[Upload Service]
        US --> Raw[(Raw Storage - S3)]
        US --> Q[Transcoding Queue]
        Q --> TW[Transcoding Workers]
        TW --> CDNStore[(CDN Origin<br/>multi-resolution)]
        TW --> Meta[(Metadata DB)]
    end
    subgraph Playback
        C2[Client] --> CDN[CDN Edge PoP]
        CDN -->|cache miss| Origin[(Origin Storage)]
        C2 -->|manifest request| Manifest[Manifest Service]
        Manifest --> C2
    end

Upload path: client uploads raw video (typically via a pre-signed URL directly to blob storage, the same pattern as [[Blob & Object Storage]] — not routed through application servers) → upload service publishes a transcoding job to a queue → transcoding workers process it asynchronously → outputs (multiple resolutions) land in CDN-origin storage → metadata (title, tags, uploader, status) is written once the video is ready to serve.

Playback path: client requests a manifest file for the video → fetches video chunks from the nearest CDN edge (cache hit, the overwhelmingly common case for popular content) → on a cache miss, the edge fetches from origin and caches it for subsequent requests — the same push/pull CDN mechanics as [[CDN]].

5. The Transcoding Pipeline — Why It's Async

Upload is fast (seconds to minutes depending on file size); transcoding — re-encoding the same video into 5+ resolutions — is slow (can take many minutes for a long video). Blocking the uploader until transcoding finishes would be a terrible experience for zero benefit (nobody's waiting to watch their own upload the instant it's submitted).

flowchart TD
    Raw[Raw Upload] --> MQ[Message Queue]
    MQ --> T1[Transcode 360p]
    MQ --> T2[Transcode 720p]
    MQ --> T3[Transcode 1080p]
    MQ --> T4[Transcode 4K]
    MQ --> TH[Thumbnail Extraction]
    MQ --> CAP[Captioning]
    T1 & T2 & T3 & T4 & TH & CAP --> Done[Mark video<br/>ready to serve]

DAG-based processing: the independent sub-tasks (transcode to each resolution, extract thumbnails, generate captions) run in parallel, not sequentially — they don't depend on each other, only on the raw upload being available. This is a direct application of the async-decoupling rationale in [[Message Queues & Kafka]]: the transcoding queue absorbs the entire spike of "500 hours of video landed this minute" without the upload service itself needing to scale to match transcoding throughput.

6. Adaptive Bitrate Streaming (ABR) — HLS/DASH

The video is pre-split into short chunks (2–10 seconds each) at every quality tier during transcoding, and a manifest file lists the chunk URLs for each tier. The player, not the server, decides which quality to request chunk by chunk, based on recently observed download speed:

class ABRPlayer:
    def __init__(self, available_bitrates: list[int]):
        self.available_bitrates = sorted(available_bitrates)   # e.g. [400, 800, 1500, 3000, 6000] kbps
        self.recent_chunk_speeds: list[float] = []              # kbps, last N chunk downloads

    def record_chunk_download(self, chunk_size_kb: float, download_time_s: float) -> None:
        speed_kbps = chunk_size_kb / download_time_s
        self.recent_chunk_speeds.append(speed_kbps)
        self.recent_chunk_speeds = self.recent_chunk_speeds[-5:]   # sliding window, last 5 chunks

    def next_chunk_bitrate(self) -> int:
        if not self.recent_chunk_speeds:
            return self.available_bitrates[0]                      # start conservative
        avg_speed = sum(self.recent_chunk_speeds) / len(self.recent_chunk_speeds)
        safe_bitrate = avg_speed * 0.8                               # safety margin, don't max out bandwidth
        candidates = [b for b in self.available_bitrates if b <= safe_bitrate]
        return candidates[-1] if candidates else self.available_bitrates[0]

Why the 0.8 safety margin, and why per-chunk not once per video: bandwidth genuinely fluctuates mid-playback (a viewer walking between WiFi zones, network congestion) — deciding quality once at the start and never adjusting would mean either buffering (chose too high) or wasted quality (chose too low and never re-evaluates) for the whole rest of the video. Re-deciding every 2–10 seconds is what makes ABR actually adaptive; the safety margin avoids oscillating right at the edge of available bandwidth, which would otherwise cause frequent visible quality flips.

7. CDN Strategy

Edge PoPs (Points of Presence) in many geographic locations cache video chunks close to viewers — see [[CDN]] for the general push/pull mechanics. Specific to video: cache key = video_id + quality + chunk_number (each chunk of each quality tier is cached independently, since a viewer might switch quality mid-video), with a long TTL (24–48 hours is reasonable — video content, unlike a news article, essentially never changes after upload, making it some of the most cache-friendly content on the internet). Popular videos are effectively always warm in cache across most edges; a brand-new or unpopular video is cached on first request and can propagate to nearby edges from there.

8. Metadata & Search

  • Metadata (title, description, uploader, status): a relational store (PostgreSQL) — this data genuinely benefits from structured queries and relationships (uploader → their videos), unlike the KV-shaped access patterns elsewhere in this KB — see [[SQL vs NoSQL]] for when SQL is still the right call.
  • Search (full-text on titles/tags/transcripts): a dedicated search index (Elasticsearch) — full-text relevance ranking isn't what a relational DB's indexes are built for; this is a deliberately separate system from the metadata store, kept in sync via the same event-driven update pattern as everything else in this pipeline.
  • View counts: exactly the same batching pattern as the News Feed problem's like counts — don't write to the DB on every view (a viral video would thunder the write path); INCR in [[Caching & Redis]], batch-flush the aggregate every ~minute. Displayed counts are therefore approximate by design, which is an explicit, acceptable trade for this specific field.

9. Deep Dives / Follow-ups

  • Live streaming vs. VOD: live streaming can't pre-transcode ahead of time (the content doesn't exist yet) — it transcodes in near-real-time as the stream arrives, trading some additional end-to-end latency (typically 10-30+ seconds behind true "live") for the same ABR-chunked delivery mechanism, and cannot rely on long-TTL edge caching the way VOD content can since each chunk is only relevant briefly.
  • DRM / content protection: encrypted chunks with per-session decryption keys issued by a separate licensing service — orthogonal to the transcoding/CDN pipeline itself, layered on top of it.
  • Recommendations (explicitly scoped out in §2 unless asked): a genuinely separate ML-serving system reading from view/engagement event streams — worth a one-sentence acknowledgment that it exists and is out of scope, rather than either ignoring it entirely or accidentally scope-creeping the interview into a recommendation-systems design.

10. Staff-Level Lens

The naive design transcodes a video into one quality tier and serves that to everyone — it works in a demo on a fast connection and fails the moment a real viewer is on a train or in a country with slower average mobile bandwidth, which is most of the world's actual viewership. Recognizing that playback quality must adapt to the individual viewer's live network conditions, not be a single fixed encode, is the concrete design decision (ABR) that separates a "video hosting" toy from a real streaming platform — naming this trade-off (storage/transcoding cost of maintaining 5 quality tiers vs. one) unprompted is the Staff-level signal here.

11. Interview Drill Questions

  1. Why transcode into 5 separate quality tiers upfront instead of transcoding on-demand when a viewer requests a specific quality? — At a 1000:1 read:write ratio, the same video is watched enormously more often than it's uploaded — paying the transcoding cost once upfront (amortized across millions of future views) is far cheaper in aggregate than re-transcoding live per playback request, which would also add unacceptable playback latency.
  2. A viewer's bandwidth drops mid-video. Walk through what happens. — The ABR player's sliding window of recent chunk download speeds reflects the drop within a few chunks; next_chunk_bitrate() selects a lower available bitrate for the next chunk request — quality steps down smoothly rather than the video buffering while waiting for a chunk too large for current bandwidth.
  3. Why is view count "approximate" acceptable here but a payment amount would never be? — This is the [[CAP Theorem & Consistency Models]] distinction made concrete: a view count being off by a handful for a few seconds has zero real consequence for any party, while an incorrect payment amount is a direct financial harm — the tolerance for eventual consistency is a property of the specific field's real-world stakes, not a system-wide default.
  4. How would live streaming change this architecture? — Transcoding can't happen ahead of time since the content doesn't exist yet — it happens in a near-real-time pipeline as the stream is ingested, adding meaningful end-to-end latency versus VOD, and edge caching is far less effective since each chunk is only relevant for a short live window rather than being requested repeatedly over days.
  5. Why cache video chunks by video_id + quality + chunk_number instead of just video_id? — A single video has multiple quality tiers and many chunks, and a viewer's ABR player can request any combination depending on their bandwidth history — caching at the video level alone would either cache far more data than needed (every quality, every chunk, regardless of demand) or force a cache miss whenever quality switches; per-chunk-per-quality keys let the CDN cache exactly what's actually being requested.

Cross-links

[[Blob & Object Storage]] · [[CDN]] · [[Caching & Redis]] · [[Message Queues & Kafka]] · [[SQL vs NoSQL]] · [[CAP Theorem & Consistency Models]]