Scale Interview
System DesignHard

Design a Video Streaming Service

YouTube / Netflix System Design

Overview

A video streaming service lets people upload videos and lets viewers watch them smoothly anywhere in the world — YouTube, Netflix, Vimeo, and Twitch's video-on-demand are all variations on this idea. A creator uploads one large file; later, millions of viewers play it back on phones, laptops, and TVs, on connections ranging from fast home fiber to a shaky mobile signal on a train.

It is labeled "Hard" because two very different problems collide. First, the uploaded file is huge and unplayable as-is across every device and network, so it must be transcoded into many versions in the background — heavy, slow, CPU-bound work. Second, serving the video back to a global audience is dominated not by storage but by egress (the bandwidth of sending video out to viewers), which is the single largest cost in the whole system and only stays fast and cheap with a CDN.

In this walkthrough you'll scope the service, size it (the storage and egress numbers drive almost every decision), model the data, design the upload and playback contract, draw the upload/processing and playback paths, and then examine the transcoding pipeline, delivery at scale with adaptive streaming, and the storage/cost tradeoffs.

Functional requirements

These are the things the system must do. Keep the list short and get the interviewer to agree before building.

  • Upload a video. A creator uploads one large source file plus metadata (title, description).
  • Process it for playback. The system converts the source into the formats, resolutions, and bitrates needed to play smoothly on any device and network.
  • Stream / play a video. A viewer plays a video and can seek (jump to any point) with quality that adapts to their network.
  • List / browse metadata. Show a video's title, description, thumbnail, and duration, and list a creator's videos.

Out of scope (state it): recommendations, comments, monetization/ads, and deep search. We keep upload, processing, and playback firmly in scope — they are the whole problem.

Tip

A strong opening: "I'll focus on upload, the processing pipeline, and adaptive playback with seeking, plus basic metadata. I'll leave recommendations, comments, ads, and search out of scope unless you'd like to include them." Naming what you cut shows product judgment — then move on to the design.

Non-functional requirements

These are the qualities the system must have. For a streaming service the dominant ones are:

  • Smooth playback that adapts to the network. Video should start quickly and play without stalling, automatically dropping to a lower quality on a slow connection and rising again when bandwidth recovers. This is the headline viewer experience.
  • Massive durable storage and huge egress. The catalog is petabytes; outbound bandwidth to viewers is enormous and is the cost that dominates the design.
  • Very read-heavy. A video is uploaded once and watched millions of times. Optimize the read (playback) path above everything.
  • Global low latency. Viewers are everywhere; the first video segment should arrive quickly no matter the continent, which means serving from somewhere physically close.
  • Highly available reads; uploads can be slower. A viewer must always be able to watch; an upload taking a few minutes to become playable is fine.

Note

Notice what is not on this list: strong consistency. Unlike a ticket system or a bank, a video being watchable a minute after upload — or a view count being slightly stale — is perfectly acceptable. This frees you to cache aggressively and process asynchronously.

Estimations

State your assumptions out loud; the goal is to show how the numbers force the architecture (blob storage + CDN, async transcoding, read-heavy caching), not to divide perfectly.

Rounded assumptions

  • Uploads: ~1M videos/day → about ~12 uploads/sec (writes are rare).
  • Average source video: ~10 minutes, and after the platform produces multiple versions, all versions of one video together average ~1 GB of stored media.
  • Viewers: ~100M videos watched/day, peaking around ~5M concurrent viewers.
  • Average playback bitrate: ~5 Mbps (a typical 1080p stream).

Storage

  • ~1M videos/day × ~1 GB each = ~1 PB added per day → on the order of ~100s of PB over a few years. This is huge but cheap per byte, append-only, and never edited in place.

Egress (the number that drives the design)

  • 5M concurrent viewers × 5 Mbps = ~25 Tbps of sustained outbound bandwidth at peak.
  • Serving that from your own data centers is impractical and ruinously expensive. The math alone tells you a CDN is mandatory, not optional.

How the numbers affect the design

Signal from the numbers Design decision
Files are huge (~GB each), 100s of PB total Store media as blobs in object storage, not in a database.
Egress is ~25 Tbps and dominates cost Serve all media through a CDN; your servers never stream bytes directly to viewers.
Reads vastly outnumber writes (watch ≫ upload) Optimize and cache the playback path first; uploads can be slower.
Transcoding is heavy and slow Do it asynchronously with a job queue and worker pool, off the upload request path.
Networks vary wildly per viewer Produce multiple bitrates and let the player switch per segment (adaptive streaming).
Metadata is small but read constantly Keep it in a separate database and cache the hot rows.

Caution

Do not anchor the whole design on storage size. Storage is large but cheap and simple. The two genuinely hard parts are the transcoding pipeline and egress delivery at scale — spend your time there.

Core entities / data model

The single most important modeling decision is to separate small structured metadata (a database) from large media bytes (blob storage). The database stores facts about a video; the blob store holds the video itself, addressed by a key.

Entity Key fields
Video video_id (PK), owner_id, title, description, duration, thumbnail_url, status, created_at
Rendition rendition_id, video_id, resolution (e.g. 1080p), bitrate, codec, manifest_key
MediaObject blob_key, rendition_id (nullable), type (segment / manifest), size — references into blob storage
UploadJob job_id, video_id, status (QUEUEDTRANSCODINGSUCCEEDED / FAILED)

A few points worth saying out loud:

  • Video.status gates playback. A video moves UPLOADING → PROCESSING → READY (or FAILED). UploadJob.status is the worker's internal state for the processing work (QUEUED → TRANSCODING → SUCCEEDED/FAILED). Keep those two state machines separate: the player only gets a playable manifest once Video.status is READY; before that the UI shows "processing."
  • A rendition (also called a variant) is one version of the video — for example 1080p at 5 Mbps, 720p at 2.5 Mbps, 480p at 1 Mbps. One video has many renditions; that fan-out is the heart of the pipeline.
  • The media never lives in the database. The database stores blob_key strings (pointers); the actual bytes live in blob storage. A database row is a few hundred bytes; a video is a gigabyte. Mixing them would make the database impossible to scale.
  • The master manifest belongs to the Video, not a single rendition. Per-rendition playlists (e.g. 720p.m3u8) attach to their Rendition, but the master manifest lists all renditions, so its MediaObject has rendition_id set to null and is associated with the Video itself.

Warning

Do not store video bytes in your metadata database (e.g. as a BLOB column). Databases are built for many small structured rows, not gigabyte files. Keep media in blob storage and store only the key in the database.

The metadata database is read-heavy with simple lookups by video_id or owner_id, so it caches well and can be a relational store or a wide-column NoSQL store — either works because the access pattern is simple.

API design

Three things matter: starting an upload, reading metadata, and getting the playback manifest the player streams from. Upload and metadata are ordinary endpoints; playback is better framed as a client contract — the player follows a defined sequence rather than calling one REST route to "get a video."

Initiate upload

POST /api/videos
Body: { "title": "My trip", "description": "...", "filename": "trip.mov", "size_bytes": 850000000 }
201 Created
Returns: { "video_id": "v_123", "upload_url": "https://blob.../v_123?signed...", "status": "UPLOADING" }

Create the metadata row in UPLOADING and return a signed upload URL that lets the client upload the file directly to blob storage — the gigabytes never pass through your application servers. For large files the upload is resumable (chunked): the client uploads parts and can retry just a failed part instead of restarting (deep dive 1).

Get video metadata

GET /api/videos/{id}
200 OK
Returns: { "video_id": "v_123", "title": "...", "duration": 612, "status": "READY", "manifest_url": "https://cdn.../v_123/master.m3u8" }

Return the facts about a video plus, once status is READY, the URL of its manifest. If the video is still processing, status is PROCESSING and there is no manifest yet — the client shows a "processing" state and polls.

Playback (client contract)

Playback is not a single "stream the video" call. The player follows a sequence, and almost all of it is served by the CDN:

1. GET master.m3u8   (the manifest)        ← lists the available renditions
   Returns: 1080p → 1080p.m3u8, 720p → 720p.m3u8, 480p → 480p.m3u8

2. GET 720p.m3u8     (a rendition playlist) ← lists that quality's ordered segments
   Returns: seg0.ts, seg1.ts, seg2.ts, ...  (each ~2–6 seconds of video)

3. GET seg0.ts, seg1.ts, ...               ← the actual media, fetched in order
   (player measures download speed and may switch to 1080p or 480p for the next segment)

This is segmented adaptive streaming (the open standards are HLS and DASH). Two ideas make it work, and both come straight from how the video is stored:

  • The video is split into many short segments (e.g. 2–6 seconds each), not served as one giant file. Seeking to minute 8 means fetching the segments around minute 8 — no need to download from the start.
  • Each quality level has its own segment list, and the player chooses per segment. It measures how fast the last segment downloaded and picks the bitrate for the next one. That per-segment choice is adaptive bitrate streaming (deep dive 2).

Note

The manifest and segments are just files in blob storage served through the CDN. The player does the adapting; the server only hosts files. This is why playback scales so well — there is no per-viewer server-side streaming session to maintain.

High-level architecture

Two paths matter and they are almost completely separate: a heavy, asynchronous upload + processing path (write), and a light, cache-everything playback path (read). We'll walk each, then combine them.

1. Upload + processing path

The creator uploads once; the system then does slow background work to make the video playable everywhere.

flowchart LR
    Creator[Creator] -->|"1. POST /api/videos"| App[Upload Service]
    App -->|"create row (UPLOADING) + signed URL"| MetaDB[(Metadata DB)]
    Creator -->|"2. upload file directly (resumable)"| Blob[(Blob Storage - source)]
    Blob -->|"3. upload complete event"| Queue[Transcoding Job Queue]
    Queue -->|"4. pull job"| Workers[Transcoding Workers]
    Workers -->|"5. write renditions + manifests"| Blob2[(Blob Storage - output)]
    Workers -->|"6. mark READY"| MetaDB
    Blob2 -.->|"7. distribute / fill on first request"| CDN[(CDN)]
  1. The creator calls the Upload Service, which creates the metadata row in UPLOADING and returns a signed upload URL.
  2. The client uploads the source file directly to blob storage using that URL — the bytes never touch your app servers. Large uploads are chunked and resumable.
  3. When the upload finishes, blob storage emits an event that enqueues a transcoding job (the upload doesn't wait for processing).
  4. Transcoding workers pull jobs from the queue. This pool scales independently of everything else, because transcoding is CPU-heavy and bursty.
  5. Each worker transcodes the source into multiple renditions (resolutions/bitrates), then packages each rendition — chops it into short segments and writes a manifest per rendition plus a master manifest — and stores them all back in blob storage. Transcoding is the CPU-heavy encode; packaging is the cheap segment-and-index step (deep dive 1).
  6. On success the worker marks the video READY. Now it can be played.
  7. Output media is distributed to the CDN (pushed ahead of time for popular content, or pulled and cached on the first viewer request).

2. Playback / read path

Playback is read-heavy and served almost entirely from the CDN — your application servers handle only the small metadata lookup.

flowchart LR
    Player[Player] -->|"1. GET /api/videos/id"| App[Metadata Service]
    App -->|"hit"| Cache[(Metadata Cache)]
    Cache -.->|"miss"| MetaDB[(Metadata DB)]
    App -->|"2. manifest_url"| Player
    Player -->|"3. GET manifest + segments"| CDN[(CDN edge)]
    CDN -->|"3a. edge hit: serve bytes"| Player
    CDN -.->|"4. edge miss: fetch once from origin"| Blob[(Blob Storage - origin)]
  1. The player asks the Metadata Service for the video's details; that small lookup is served from cache, falling back to the metadata DB on a miss.
  2. The response includes the manifest_url (a CDN URL).
  3. The player fetches the manifest, then the segments, from the nearest CDN edge. On an edge hit (the common case for popular videos) the CDN serves the bytes directly — your origin is never touched. For non-public content these manifest/segment URLs are typically signed with expiring tokens (and optionally DRM-gated), so segments aren't world-readable or hotlinkable.
  4. On an edge miss the CDN fetches the file once from blob storage (the origin), caches it at the edge, and serves it; later viewers in that region hit the cache.

Important

Notice the two paths share almost nothing on the hot path. Uploading touches your app servers and workers; watching is mostly the player ↔ CDN, with one tiny metadata call to your servers. That separation is what lets reads scale to billions while writes stay modest.

3. Combined architecture

Putting it together — the asynchronous pipeline on the left, the cached read path on the right, joined only by blob storage and the metadata DB:

flowchart LR
    Creator[Creator] -->|"initiate"| App[Upload Service]
    App --> MetaDB[(Metadata DB)]
    Creator -->|"direct resumable upload"| Blob[(Blob Storage)]
    Blob -->|"complete event"| Queue[Transcoding Job Queue]
    Queue --> Workers[Transcoding Workers]
    Workers -->|"renditions + manifests"| Blob
    Workers -->|"mark READY"| MetaDB
    Blob -.->|"origin"| CDN[(CDN edge)]
    Player[Player] -->|"metadata"| Meta[Metadata Service]
    Meta --> Cache[(Metadata Cache)]
    Cache -.-> MetaDB
    Player -->|"manifest + segments"| CDN
    CDN -.->|"miss: origin fetch"| Blob

The write side (upload → queue → workers → blob) is decoupled from the read side (player → metadata + CDN) by the queue and the blob store. You can scale transcoding workers during an upload spike without touching playback, and absorb a viral video on the CDN without touching the pipeline.

Tip

Separate the heavy asynchronous processing path from the cached playback path, and serve all media through a CDN. These two ideas carry most of the design.

Deep dives

1. Upload and the transcoding pipeline

Two problems live here: getting a huge file in reliably, and turning it into something playable everywhere — without making the user wait.

Resumable, direct upload. A 1 GB upload over a flaky connection will sometimes drop. Two rules make it robust:

  • Upload directly to blob storage via a signed URL, so the gigabytes bypass your application servers entirely (which would otherwise melt under the bandwidth).
  • Chunk the file (e.g. into 5–10 MB parts) and upload parts independently. If part 37 fails, the client retries only part 37, not the whole file. Once every part is uploaded, the client (or your server) issues an explicit complete-multipart call listing the parts and their ETags; that finalizes the object and fires the completion event that triggers transcoding.

Why transcoding is asynchronous. Re-encoding a 10-minute video into several resolutions is CPU-heavy and can take minutes — far longer than any HTTP request should block. So the upload returns immediately, an event enqueues a transcoding job, and a separate worker pool does the work. The creator sees the video as "processing" and gets notified (or the page polls status) when it turns READY.

Two distinct stages: transcode, then package. It is worth separating the two steps the words "process the video" hide. Transcoding is re-encoding the source into each codec/resolution/bitrate — the CPU-heavy part. Packaging is the cheap step that chops each encoded rendition into HLS/DASH segments and writes the manifests; it does almost no compute, just splitting and indexing. The pipeline is transcode (encode renditions) → package (segment + manifests):

flowchart TB
    Source["source.mov<br/>(1080p, 1 GB)"] -->|"transcode = re-encode (ffmpeg)"| R1080["1080p @ 5 Mbps"]
    Source --> R720["720p @ 2.5 Mbps"]
    Source --> R480["480p @ 1 Mbps"]
    R1080 -->|"package: split into ~4s segments + manifest"| P1080["1080p.m3u8<br/>seg0, seg1, ..."]
    R720 --> P720["720p.m3u8<br/>seg0, seg1, ..."]
    R480 --> P480["480p.m3u8<br/>seg0, seg1, ..."]
    P1080 --> Master["master.m3u8<br/>(lists all three)"]
    P720 --> Master
    P480 --> Master

The parallelism lives in the encode: a large job is often split by time so many workers transcode different chunks of the same video at once, then the outputs are stitched together and packaged — this is how a long video becomes playable in minutes rather than hours.

Modern pipelines package once as CMAF (fragmented-MP4) segments referenced by both HLS and DASH manifests, so you store one segment set instead of duplicating .ts for HLS and .mp4 for DASH — a real storage and cost win at catalog scale.

Orchestration and retries. The pipeline is a series of steps (validate → transcode each rendition → package into segments + manifests → mark READY) coordinated by a job orchestrator. Steps are idempotent and retryable: the retry unit is the failed chunk/rendition, re-encoded independently without redoing the ones that already succeeded, and its output overwritten. Retry is safe because steps are idempotent — a failed rendition/chunk is simply re-encoded from scratch and its output overwritten; don't rely on byte-identical output across runs, since multi-threaded and hardware encoders aren't bit-for-bit deterministic. If a rendition keeps failing, the job is marked FAILED and the creator is told, rather than leaving the video stuck forever.

Warning

Do not transcode inside the upload request. Encoding several renditions takes minutes; blocking the request would time out and waste a server thread per upload. Always enqueue a job and process it on a separate worker pool.

Caution

Do not forget the status lifecycle. If you mark a video playable before all renditions are written, the player can request a manifest that points at segments that don't exist yet. Only flip to READY after the outputs are durably stored.

2. Delivery at scale — CDN and adaptive bitrate

This is where the egress numbers come home. At ~25 Tbps peak, the delivery design is the system.

Why a CDN is mandatory. A CDN is a fleet of cache servers (edges) spread across the world. Your blob storage is the origin; edges fetch from it once and then serve many nearby viewers from cache. This does two things at once:

  • Offloads egress. A popular video is fetched from your origin a handful of times (once per edge region) and served from edge caches millions of times. Without a CDN, your origin would have to push all ~25 Tbps itself — impossible at sane cost.
  • Cuts latency. Bytes travel from a nearby city instead of across an ocean, so playback starts fast and segments arrive in time to avoid stalls.
flowchart LR
    Origin[("Blob Storage<br/>(origin)")] -->|"origin fetch (once per region)"| Tokyo["CDN edge (Tokyo)"]
    Origin -->|"origin fetch (once per region)"| Frankfurt["CDN edge (Frankfurt)"]
    Origin -->|"origin fetch (once per region)"| Virginia["CDN edge (Virginia)"]
    Tokyo -->|"edge serves many"| JP["thousands of viewers in Japan"]
    Frankfurt -->|"edge serves many"| EU["thousands of viewers in Europe"]
    Virginia -->|"edge serves many"| US["thousands of viewers in the US"]

Adaptive bitrate streaming. Because the video exists as a manifest plus segments at several bitrates, the player can change quality mid-playback. It downloads a segment, measures the throughput, and picks the bitrate for the next segment: bandwidth dropped → fetch 480p next so playback doesn't stall; bandwidth recovered → climb back to 1080p. The viewer sees quality shift instead of a spinning buffer. This only works because of how the media is stored — segmented, with one playlist per quality.

For the player to swap quality at any segment boundary, package all renditions with segment boundaries starting on an aligned IDR/keyframe (closed GOP), so segment N of 1080p covers the exact same time range as segment N of 480p — renditions can still differ in their internal GOP structure. Then switching means simply fetching the next segment from a different playlist, with no gap or overlap.

Warning

Misaligned segment boundaries break adaptive switching. If 1080p and 480p are cut at different points, segment N no longer lines up across renditions, and switching mid-stream produces a visible glitch or a stall while the player resynchronizes. Package all renditions with segment boundaries starting on an aligned IDR/keyframe (closed GOP) so their segments align.

Progressive download — serve one file and seek with byte ranges (avoid at scale)

Store each video as a single file per quality and let the player download it (using HTTP byte-range requests to seek). The player picks one quality at the start — there is no switching once playback begins.

Worked example — a viewer on a train starts a 1080p file:

 start: pick 1080p (5 Mbps) — looks fine on wifi
 train enters a tunnel → bandwidth drops to 1 Mbps
 player is committed to the 1080p file → buffer drains → ⏳ STALL (spinner)
 (no way to drop to 480p without restarting playback)
  • Pro: dead simple — just files and HTTP range requests; trivial to host and seek within one quality.
  • Con: the quality is fixed for the whole session. When the network degrades, the player can only stall and buffer; it cannot drop to a lower bitrate. A poor experience on the variable mobile networks most viewers use.
  • Verdict: avoid at scale — acceptable for a small site with controlled networks, but wrong for a global, mobile-heavy audience.
Adaptive segmented streaming (HLS / DASH) — the standard at scale

Store each quality as many short segments plus a manifest. The player fetches segments in order and re-picks the bitrate for each one from measured bandwidth, so quality tracks the network in real time and seeking just fetches the segments around the target time.

Worked example — same train, same video, adaptive:

 start on wifi: fetch 1080p seg0, seg1 (downloads fast) → stay 1080p
 tunnel, bandwidth → 1 Mbps: seg7 downloaded slowly → switch to 480p for seg8
 seg8, seg9 at 480p download in time → playback continues, no stall (lower quality)
 out of tunnel, bandwidth back: seg12 fast again → climb back to 1080p
 viewer seeks to minute 8 → fetch the segments at minute 8 directly (no full download)
  • Pro: smooth playback across changing networks, fast start (begin at a low bitrate then climb), and cheap precise seeking. It is a perfect fit for a CDN, since every segment is an independently cacheable file.
  • Con: more to produce and store (many segments and manifests per video) and more moving parts in the pipeline. That cost is paid once at transcode time and amortized over millions of views.
  • Verdict: the industry standard and the expected interview answer — the extra processing buys the adaptivity, seeking, and cacheability the product needs.
Approach Quality switching Seeking CDN fit Best when
Progressive download None (fixed per session) Byte-range on one file OK Small scale, stable networks
Adaptive segmented (HLS/DASH) Per segment, automatic Fetch nearby segments Excellent (each segment cacheable) Global, mobile, at scale

Caution

Do not propose streaming video directly from your application servers or database. Media must be served as cacheable files from a CDN; routing ~25 Tbps of segments through your own servers is neither affordable nor fast enough.

Warning

Pick a segment length deliberately. Very short segments (1–2s) adapt and start faster but create many more files and more request overhead; longer segments (6–10s) are more efficient but slower to adapt and seek. Around 2–6 seconds is the usual compromise.

3. Storage, cost, and metadata read scale

Three cost-and-scale realities shape this layer.

Blob storage for media; a database for metadata. Media goes in blob storage because it is cheap per byte, effectively unlimited, durable (the provider replicates each object across machines and sites), and accessed by a simple key — exactly the append-only, large-file pattern blob stores are built for. Metadata goes in a database because it is small, structured, and queried (by video_id, by owner_id). Keeping them apart lets each scale on its own terms.

Cost is dominated by storage + egress, not compute. Transcoding is a one-time CPU cost per video; storage and egress are forever and grow with the catalog and the audience. Two levers matter:

  • Egress is the biggest line item, which is the entire economic reason for the CDN — a cache hit at the edge is far cheaper than an origin fetch, and most playback is cache hits for popular content. Popular videos reach near-100% edge hit ratios, but the long tail sits near 0%: edges can't hold the whole catalog, so they cache on demand and evict (LRU/TTL), and a rare long-tail view simply costs one origin fetch.
  • Storage tiering keeps the bill sane: hot, popular videos stay on fast storage and warm CDN caches; cold videos that almost nobody watches move to cheaper, slower archival storage. You don't pay premium storage rates for a decade-old clip with two views a year. This mirrors the CDN's own behavior — the long tail isn't worth keeping warm anywhere, so it falls back to cheap, slow storage and is fetched on demand.

Caching hot metadata. Playback always does one small metadata lookup, and a few popular videos get a hugely disproportionate share of views (a Pareto distribution). Cache that hot metadata (title, duration, manifest_url) so the metadata database isn't hit for every play. The pattern is cache-aside — check the cache, fall back to the DB on a miss, and fill the cache — the same idea as caching segments at the CDN edge, one layer up. Watch for the cache stampede case: when a video suddenly goes viral, its first cache miss can send thousands of concurrent plays straight to the metadata DB at once — coalesce them with single-flight (only one request fills the cache while the rest wait) or keep the hot entry warm so it never expires under load.

View counts (a sub-point). Incrementing a counter on every play would hammer the database at read scale. Instead emit a view event to a stream and aggregate counts asynchronously; a slightly stale count is fine for this product. (The full analytics pipeline is an extension below.)

Warning

Do not skip caching metadata in a read-heavy system. Even though each lookup is tiny, millions of plays per minute against the metadata database will overwhelm it without a cache in front.

Tradeoffs & bottlenecks

  • CDN cost vs origin load. The CDN is expensive, but serving everything from your origin is far more expensive and far slower. The real tuning is the cache hit rate: long-lived caching for immutable segments maximizes hits; the rare unpopular video that misses simply costs one origin fetch.
  • Number of renditions: storage vs reach. More renditions (e.g. add 1440p, 4K, very-low-bitrate mobile) mean better-fitting quality for every device, but more transcoding compute and more stored bytes per video. Encode the ladder your audience actually uses.
  • Segment length: adaptivity vs overhead. Short segments start and adapt faster but multiply file count and request overhead; long segments are efficient but sluggish to adapt and seek. ~2–6s balances them.
  • Processing latency vs cost. Splitting a video across many parallel workers makes it playable sooner but uses more machines at once. Tune parallelism to how fast creators expect publishing to be.
  • Push vs pull CDN fill. Pre-pushing popular content to edges avoids a first-viewer miss but wastes storage on videos nobody watches; pull-on-demand is cheaper but the first viewer in a region pays an origin fetch. Most systems pull by default and pre-push known-hot content (premieres).
  • Consistency is intentionally relaxed. A video being watchable a minute after upload, or a view count lagging, is acceptable — and that relaxation is exactly what enables async processing and heavy caching.

Extensions if asked

If the interviewer wants to push beyond the core, add only the extension that changes the design discussion. These are standalone topics, not covered in depth here:

  • Recommendations. Ranking what to watch next is a large machine-learning system of its own (candidate generation plus ranking on watch-history signals), entirely separate from delivery. There's no dedicated guide for it yet.
  • View-count and watch-time analytics. Emit play/heartbeat events to a stream and aggregate them in a separate analytics pipeline, never by writing to the hot metadata database on each play.
  • Live streaming. Real-time ingest changes the pipeline: you transcode and segment a continuous feed with a few seconds of latency and a sliding manifest, instead of processing a finished file. The delivery (segments + CDN) is similar, but the ingest and latency constraints are different.
  • DRM and content protection. Encrypting segments and gating playback with per-viewer license keys to stop unauthorized copying — relevant for paid catalogs like Netflix, less so for open user-generated content.
  • Thumbnails and preview frames. Generate poster images and the hover-scrub filmstrip during transcoding, stored as small blobs served from the CDN.

What interviewers look for & common mistakes

What interviewers usually reward:

  • Separating media from metadata — blob storage for the large files, a database for the small structured rows, with the blob key stored in the database.
  • Asynchronous transcoding — recognizing it's CPU-heavy and slow, so it goes on a queue + worker pool with a status lifecycle, never inside the upload request.
  • The egress-driven CDN argument — deriving the bandwidth number and concluding a CDN is mandatory, with origin/edge caching.
  • Adaptive bitrate streaming — explaining segments + manifest let the player switch quality per segment, and why that beats a single progressive file.
  • Read-heavy optimization — caching hot metadata and serving all media from the edge, keeping the playback path off your origin.

Before you finish, do a quick mistake check:

  • Did you keep video bytes out of the database and in blob storage?
  • Did you make transcoding asynchronous with a job queue and retries?
  • Did you justify the CDN from the egress numbers, not just name-drop it?
  • Did you explain segmented adaptive streaming (manifest + segments + per-segment bitrate choice)?
  • Did you use a signed, resumable, direct-to-blob upload rather than routing gigabytes through your app servers?
  • Did you cache hot metadata and keep view counting off the synchronous read path?

Practice this live

Run this exact question with our AI voice interviewer and get feedback.

Start the interview →