← Problems

Video Streaming Platform (YouTube / Netflix)

hardsenior / staff
Start timed practice

Upload, transcode and stream video globally with adaptive bitrate and low start-up latency.

Requirements

Functional

  • Creators upload video files (up to several GB) with a title, description and visibility.
  • Viewers stream any video on web, mobile and TV with adaptive quality.
  • Playback resumes where the viewer left off, across devices.
  • View counts, likes and basic creator analytics.
  • Optional: live streaming, comments, recommendations, subtitles, DRM.

Non-functional

  • Start-up latency: first frame within ~1–2 s on a normal connection.
  • No rebuffering under fluctuating bandwidth: the player must switch quality smoothly.
  • Highly available playback; a transcoding outage delays new uploads but never breaks existing videos.
  • Durable: an uploaded original is never lost (11 nines on the object store).
  • Global: viewers on every continent get similar start-up latency.
  • Upload-to-playable within minutes for typical videos.

Out of scope

  • Recommendation ranking, ad insertion, monetisation, moderation workflow details, live streaming internals.

Back-of-envelope estimates

Assumptions

  • 1B monthly viewers, 100M DAU, each watches ~5 videos/day of ~5 min average.
  • 500K uploads/day, average 10 min long, raw file ~1 GB (phone 1080p, ~15 Mbps).
  • Store 5 renditions: 1080p (5 Mbps), 720p (2.5), 480p (1.2), 360p (0.8), 240p (0.4) ≈ 10 Mbps total ≈ 75 MB per minute.

Upload traffic

  • 500K/day / 86,400 ≈ 6 uploads/s average, ~20/s peak.
  • Ingest bandwidth: 500K × 1 GB/day = 500 TB/day ≈ 46 Gbps average. Direct-to-object-storage multipart, never through API servers.

Storage

  • Originals: 500 TB/day → 180 PB/year. Keep originals in cold storage after transcoding.
  • Renditions: 500K × 10 min × 75 MB = 375 TB/day → ~137 PB/year, hot tier for the first weeks then infrequent access.
  • 5-year total on the order of 1.5 EB; this is why cost tiering and per-title encoding matter.
  • Metadata: 500K/day × 5 years ≈ 1B videos × ~2 KB = 2 TB. Trivial; a sharded relational store.

Playback / egress

  • 100M DAU × 5 videos × 5 min = 2.5B minutes/day. At an average delivered bitrate of ~2 Mbps: 2.5B × 60 s × 2 Mbps / 8 ≈ 37.5 PB/day.
  • 37.5 PB / 86,400 s ≈ 3.5 Tbps average, ~10 Tbps peak. Only a CDN can serve this; origin egress should be <5% of it with a ≥95% edge hit rate.
  • Concurrent streams at peak: 100M × 25 min/day / 1,440 min × peak factor 3 ≈ 5M concurrent viewers.

Transcoding compute

  • Encoding ~1× realtime per rendition on one core at 1080p: 10 min video × 5 renditions ≈ 50 core-minutes.
  • 500K/day × 50 core-min = 25M core-min/day ≈ 17K cores running continuously. Chunked parallel encoding makes a 10-min video ready in ~1–2 min of wall-clock.

Cache

  • Top 20% of videos drive ~80% of views. Edge PoPs each need the hot set: a few hundred TB of SSD per PoP, backed by regional caches.

API design

POST /v1/videos

  • Body: { title, description, visibility, fileSizeBytes, contentType }
  • Returns 201 { videoId, uploadId, partSizeBytes, partUrls[] } — pre-signed multipart URLs to object storage.
  • Errors: 400 invalid metadata, 413 file too large, 429 upload quota.

PUT {partUrl} (client → object storage directly)

  • Uploads one part; storage returns an ETag per part. Retrying a part is idempotent.

POST /v1/videos/{videoId}/complete

  • Body: { uploadId, parts: [{ partNumber, etag }] }
  • Finishes the multipart upload, validates size and checksum, enqueues transcoding. Returns 202 { status: 'processing' }.
  • Errors: 409 already completed, 422 checksum mismatch.

GET /v1/videos/{videoId}

  • Returns { title, durationSec, status, thumbnails[], manifestUrl, renditions[] }.
  • manifestUrl points at the CDN (HLS .m3u8 or DASH .mpd), with a short-lived signed token for private videos.

GET {cdn}/v/{videoId}/{rendition}/seg_{n}.m4s (player → CDN)

  • Immutable segments; cached for a year. Manifests cached for minutes.

PUT /v1/videos/{videoId}/progress

  • Body: { positionSec, deviceId }. Fire-and-forget from the player every ~10 s. Returns 204.

POST /v1/videos/{videoId}/views

  • Body: { sessionId, watchedSec }. Emits an event; counts are eventually consistent.

Notes

  • All playback traffic goes to the CDN, never to API servers.
  • Use signed URLs with expiry for private or paid content; the CDN validates the signature at the edge.

Data model

Entity: video

  • video_id (PK, snowflake/ULID), owner_id (indexed), title, description, visibility
  • status (uploading | processing | ready | failed), duration_sec, original_key (object store path)
  • created_at, published_at, view_count (denormalised, updated in batches)

Entity: rendition

  • rendition_id (PK), video_id (FK, indexed), codec, height (240…1080), bitrate_kbps
  • manifest_key, segment_prefix, status

Entity: segment / manifest objects (object storage, not the DB)

  • Key: v/{video_id}/{rendition}/seg_{n}.m4s, plus master.m3u8 and per-rendition playlists.
  • Immutable once written; a re-encode produces a new rendition_id and new keys.

Entity: watch_progress

  • (user_id, video_id) PK, position_sec, device_id, updated_at
  • Written every ~10 s per active viewer: a key-value store (DynamoDB/Cassandra/Redis with persistence), not the relational metadata DB.

Entity: user

  • user_id (PK), handle, email, created_at

Storage choice

  • Video metadata: relational (Postgres/MySQL), sharded by video_id, read replicas plus a cache in front. Queries are simple and the dataset is small (a few TB).
  • Bytes: object storage (S3/GCS) with lifecycle rules: originals to cold tier after transcoding, renditions to infrequent-access tier after 30–90 days without views.
  • Progress and view events: high-write key-value and stream (Kafka) respectively; aggregate views in batch and write back to video.view_count.

High-level design

Upload path
1. Client calls POST /v1/videos. The upload service creates the metadata row with status=uploading and returns pre-signed multipart URLs.
2. The client uploads parts straight to object storage in parallel, retrying individual parts.
3. The client calls /complete. The upload service finalises the multipart object, verifies checksum and size, sets status=processing and publishes a "video uploaded" message to the transcoding queue.

Processing path
1. A transcoding orchestrator picks up the message, probes the file, and splits it into chunks at GOP boundaries.
2. Chunks are encoded in parallel across a worker fleet, one job per (chunk, rendition). Outputs are packaged into HLS/DASH segments plus manifests and written to object storage.
3. Thumbnails, audio tracks and a copyright fingerprint are produced by side tasks. When all tasks finish the orchestrator sets status=ready and pre-warms the CDN if the video is scheduled.

Playback path
1. Client calls GET /v1/videos/{id} and receives the manifest URL (CDN) and metadata.
2. The player fetches the master manifest, then segments from the CDN edge. Cache miss → regional cache → origin shield → object storage.
3. The player measures throughput per segment and switches rendition up or down.
4. Every ~10 s the player sends progress to the progress service and view events to a stream; counts and analytics are aggregated asynchronously.

Why these splits?

  • Bytes never pass through API servers: upload and playback are object-store and CDN problems, metadata is a small DB problem.
  • Transcoding is bursty and expensive; a queue decouples upload acknowledgement from processing and lets the worker fleet autoscale.

Deep dives

1. Resumable upload with pre-signed URLs

  • Multipart upload: the client splits the file into 5–50 MB parts and PUTs each to a pre-signed URL. Parts can be uploaded in parallel and retried independently; storage returns an ETag per part.
  • Resume: the client (or a GET /uploads/{id} call) asks the server which part numbers are already present and only sends the missing ones. Pre-signed URLs expire (e.g. 24 h); the server can mint new ones for the same uploadId.
  • Completion: the client sends the part list. The server verifies the assembled object's size and a client-supplied checksum (per-part MD5 or a whole-file SHA-256), runs a quick ffprobe to confirm it is decodable, then flips status to processing. Reject early rather than let a corrupt file burn transcoding minutes.
  • Abandoned uploads: a lifecycle rule aborts incomplete multipart uploads after N days so parts do not leak storage.

2. The transcoding DAG

  • Probe: read codec, resolution, duration, frame rate, keyframe positions.
  • Split: cut at GOP (group of pictures) boundaries so every chunk starts on a keyframe and can be decoded independently. Chunks of ~10–30 s balance parallelism against per-task overhead.
  • Encode: fan out one task per (chunk, rendition). Each worker is stateless: download chunk, encode, upload output. A failed chunk is retried in isolation; the whole file is never re-encoded.
  • Package: stitch encoded chunks into fixed-length segments (2–6 s), generate per-rendition playlists and the master manifest, optionally encrypt for DRM.
  • Side tasks: thumbnails and storyboard sprites, audio normalisation, subtitle extraction, fingerprinting.
  • Orchestration: a DAG scheduler (Temporal/Airflow-style or a custom state machine in the DB) tracks task state and handles idempotent retries. Priority queues: a scheduled premiere from a large creator jumps the queue; backlog from a bulk importer goes to a low-priority lane.
  • Cost: per-title encoding (pick bitrates based on content complexity) saves 20–50% of storage and egress versus a fixed ladder.

3. Adaptive bitrate (ABR)

  • The master manifest lists renditions with bandwidth and resolution. The player starts at a conservative rendition for a fast first frame, then measures throughput of each downloaded segment.
  • Decision inputs: recent throughput (EWMA), current buffer level, and screen size. Buffer-based rules ("if buffer > 20 s step up, if < 5 s step down") avoid oscillation better than throughput alone.
  • Segments align across renditions (same keyframe positions), so switching happens at a segment boundary with no visual glitch.
  • Short segments (2 s) make switching and live latency faster; longer segments (6 s) are more cache-friendly and have less manifest overhead. Pick 4 s for VOD.

4. CDN tiering, cache keys and pre-warming

  • Three tiers: edge PoP (nearest the viewer, SSD), regional cache (per continent), origin shield (one or two caches in front of object storage so a global miss hits storage once, not once per PoP).
  • Cache key: the segment URL, which includes video id, rendition and segment number. Segments are immutable, so set Cache-Control: immutable, max-age=1y. Manifests are short-lived (minutes) because renditions can be added later.
  • Hit rate: head content is in every edge; the long tail lives in regional caches. Target ≥95% at the edge so origin egress is a small fraction of total.
  • Pre-warming: for a scheduled premiere, push the first few minutes of every rendition to the edges ahead of time, and stagger start times by a few seconds per region to avoid a synchronised thundering herd.
  • Request collapsing at every tier: concurrent misses for the same segment produce one upstream fetch.

5. Watch progress, resume and view counting

  • Progress: the player writes { position } every 10 s and on pause/close. Key (user_id, video_id) in a KV store; last-writer-wins by timestamp is fine. On open, the player reads the position and seeks. Cross-device works for free because the key is the user, not the device.
  • View counting: the player emits a view event after N seconds watched. Events go to a stream; a streaming job counts per video per minute and writes to an analytics store; a periodic job updates video.view_count for display. Dedupe by (session_id, video_id) within a window to resist replay and reloads.
  • Hot videos: a viral video gets millions of events per minute; partition the stream by video_id with a salt for the hottest keys so one partition does not become the bottleneck.

6. Copyright fingerprinting at upload

  • Generate an audio and video fingerprint (perceptual hashes over short windows) as a side task of the DAG and match against a reference index of rights-holder content.
  • Matching is approximate nearest-neighbour search on hash vectors; it runs asynchronously and can block publishing, mute audio, or just attribute revenue depending on policy.
  • Keep this out of the hot upload path: the video can be "ready" for the owner while the match job finishes.

Trade-offs & wrap-up

Summary of decisions

  • Direct-to-object-storage multipart upload with pre-signed URLs; API servers never carry bytes.
  • Chunked, parallel transcoding orchestrated as a DAG with per-chunk retries and priority lanes.
  • HLS/DASH over a three-tier CDN with immutable segment URLs and an origin shield.
  • Player-driven ABR with buffer-aware switching, 4 s segments for VOD.
  • Metadata in sharded relational storage; bytes in object storage with lifecycle tiering; progress in a KV store; views via a stream.

What was given up

  • Sub-second latency (segmented HTTP streaming is seconds behind; WebRTC would be needed for interactive live).
  • Exact real-time view counts; they are eventually consistent and deduplicated approximately.
  • Simplicity: the transcoding pipeline is a distributed system of its own.

With more time

  • Live streaming with low-latency HLS, DRM and key rotation, per-title encoding with ML-driven bitrate ladders, recommendation and search, regional storage replication for data residency.