Problem library

Track 3 · Scale machinery

hard

Video Streaming

Upload once, transcode into a ladder of qualities, and serve millions of viewers from the edge. Bytes are the problem; the API is an afterthought.

10 parts · 2 workloads
CDNtranscodingobject storageadaptive bitrate

Suggested architecture

Scenario

Viewers pull bytes from the edge; your servers see manifests and misses. The two numbers that decide the bill are the CDN hit rate and how many transcoders it takes to keep up with uploads that each cost minutes of CPU.

Sessions starting per second. Each pulls a manifest, then segments — the fanout on the CDN edge.

Share of segment requests the edge answers. Popularity is extreme, so this is high — and every point below it is origin bandwidth.

Videos uploaded per second. Each is minutes of transcoding.

Each holds two jobs of about two minutes. Uploads per second × 120 s ÷ 2 is how many you need — hundreds, not tens.

Open in playground →This diagram is a playground design: the sliders write onto it and the same engine judges it. Open it to change anything, run a spike, or price it.
Entering
~20K req/s
Playback sessions
50 ms
Uploads
100 ms
Busiest
Transcoders 13%
Egress (edge)
~480 Gbps
synchronousasynchronousfallback / miss path
segmentsmissmanifest + progresslookupview eventsuploadsigned PUTtranscode jobjob6 qualities × segmentsready
Components

Click a component for its role, common technology choices and tradeoffs, and what it is carrying at this scale. Hover a connection to see what flows along it. Drag to rearrange — layout changes are local and reset on reload.

Every figure here is a rough estimate from simple capacity arithmetic, not a benchmark. Each part carries its own assumption about what one copy can do — real numbers depend on your hardware, payloads and access pattern. The point is which component moves first as you turn the dials, not the digits themselves.

In plain words

A video service stores uploads in several qualities and plays them back to millions of viewers, each of whom picks the quality their connection can handle. The surprise is where the work is: the API is tiny, the bytes are enormous, and the design is really about making sure those bytes come from a cache near the viewer instead of from you.

The numbers

Assume 20,000 sessions starting per second at peak, each pulling a 4-second segment at a time at an average 3 Mbps, and 5 uploads a second of videos that each take minutes to transcode.

segment fetches — per second of session starts, compounding
5,000 /s
egress (bytes leaving your network — what the cloud bills)
100s of Gbps
videos being transcoded at any moment
600
edge cache hit rate, because views are so skewed
98 %+

Worked the way back-of-envelope estimation describes: a few facts, a few multiplications, and the big number tells you the shape of the problem.

1 call per watchhundreds of segmentsmissessigned URL6 qualitiesViewersPlayback APImanifest: a few KBCDN edgesegments, 98% hitsObject storageevery quality of everythingTranscoderminutes per videoUploads
Bytes come from the edge; decisions come from the API. The origin sees only what the CDN does not have.

Decision 1: the CDN is the product

Viewing is a cache problem with a very skewed key distribution. A few hundred videos are most of the views on any day, so an edge cache hits in the high nineties without trying. Segments are immutable — a transcoded chunk never changes once written — so there is no invalidation, and cache lifetimes are effectively forever.

That leaves the origin — object storage holding every quality of every video — serving only misses: the first viewer of anything, and the long tail. Move the hit-rate slider and watch what a single percentage point does to the segment store. Each point is a percent of a few hundred gigabits, which is why origin egress is the line item that decides whether the business works.

Decision 2: adaptive bitrate is the player's job

Each video is transcoded into a ladder — 240p up to 4K — and every quality is cut into segments at the same time boundaries. The manifest — a table of contents, a few kilobytes — lists all of them. The player measures how fast the last segment arrived and picks the quality of the next one: down before it stalls, up when there is room.

A manifest, in spirit (HLS)
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=3000000,RESOLUTION=/index.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=6000000,RESOLUTION=/index.m3u8

# inside /index.m3u8: the same 4 s boundaries as every other quality
#EXTINF:4.0,   seg-0001.ts
#EXTINF:4.0,   seg-0002.ts
#EXTINF:4.0,   seg-0003.ts
The player's whole decisionTypeScript
function pickQuality(lastSegmentBytes: number, lastSegmentMs: number, ladder: Rendition[]) {
  const measuredBps = (lastSegmentBytes * 8) / (lastSegmentMs / 1000);
  const safe = measuredBps * 0.8;                              // leave headroom for variance
  return ladder.filter((r) => r.bandwidth <= safe).at(-1) ?? ladder[0];  // highest that fits
}

The server's role is to have every quality ready. Adaptation happens entirely on the client, per segment — which is why the design has no "quality selection service": there is nothing to decide server-side.

Decision 3: transcoding is the slow, expensive, boring part

An upload is the one write path: tiny in count, enormous in work. A ten-minute video into six qualities is minutes of CPU per copy. The transcoder is the only part in the library where an instance handles less than one request a second, and the model sizes it by slots held rather than by throughput.

  1. Upload straight to object storage

    The API hands the creator a signed URL — a link that lets one client write one object, for a short time — so no server of yours proxies gigabytes.

  2. Put the job on a queue

    The upload finishes in seconds; the minutes happen elsewhere. The creator sees "processing".

  3. Split the source into chunks and transcode them in parallel

    Sixty 10-second chunks across sixty workers: the whole ladder for a long video is ready in under a minute instead of twenty.

Issue a signed upload URL; never touch the bytes yourselfTypeScript
app.post("/uploads", async (req, res) => {
  const key = `raw/${videoId()}.mp4`;
  const url = await storage.presignPut(key, { expiresIn: 900, maxBytes: 8 * GB });
  res.json({ uploadUrl: url, key });                   // client PUTs directly to storage
});

storage.onObjectCreated("raw/", (key) => transcodeQueue.publish({ key, ladder: LADDER }));

Where this design breaks

  • A new video going viral. The first thousand viewers all miss the edge at once, and object storage allows only a few thousand requests a second per prefix (a folder-like key range). Spread segment keys across prefixes and let the CDN collapse identical simultaneous requests into one.
  • Live streaming. Segments are produced as the event happens; there is no "ready", and the edge must fetch each one within seconds of it existing. A different design with the same parts.
  • Progress writes. Every viewer reporting position every few seconds is a write rate larger than any other in the system. It goes to a pub/sub topic, not the database, and is folded down before it lands.
  • Storage. Six qualities of everything ever uploaded, forever. Tiered storage and deleting the 4K copy of videos nobody watches is a real policy, not an optimisation.

Take this with you

  • The one idea: serve bytes from the edge, decisions from the API. Cache hit rate is the number that decides whether the business works.
  • In an interview, describe the segment + manifest model, adaptive bitrate on the client, and the upload → queue → transcode path.
  • At work, anything with a heavily skewed, immutable read set (thumbnails, package downloads, map tiles) is this design. Put a CDN in front and watch the origin traffic vanish.