Case Studies
YouTube
Transcoding a raw upload into a rendition ladder, and the adaptive bitrate streaming that keeps playback smooth.
A recurring prompt especially at any media- or streaming-adjacent company — distinct from most other designs in this track because the interesting problem is a processing pipeline (transcoding) feeding a delivery mechanism (adaptive bitrate), not a database or cache trade-off.
Understanding the Problem
Think of it like a moving company that doesn't wait until moving day to figure out box sizes. It pre-packs the same shipment into a few standard sizes — small, medium, large — so whichever one fits through a customer's doorway that day is already sitting in the truck. A video system works the same way: transcode the same upload into several quality tiers ahead of time, so whichever tier fits a viewer's connection that moment is already sitting in storage, ready to stream.
A user uploads a video file. Before anyone can watch it, the system has to convert it into several different resolutions and bitrates, since viewers connect over wildly different network conditions and devices. A viewer then streams the video, and playback should keep going — adapting quality up or down — rather than stalling to rebuffer whenever available bandwidth changes. The two hard problems here are largely separable: getting a raw upload transcoded into a deliverable form, and delivering that form globally without every playback hitting origin storage directly. Let's pin down exactly what we're on the hook to build before tackling either one.
Functional Requirements
- Upload a video; it becomes available for streaming once processing completes.
- Stream a video, with playback quality adapting automatically to the viewer's current network conditions rather than staying fixed.
- Retrieve basic video metadata (title, duration, uploader) for display.
Out of scope
- Live streaming — a genuinely different real-time problem, since there's no already-stored raw file to transcode from ahead of time.
- Recommendations, comments, and search.
Non-Functional Requirements
With scope fixed, here's what we're actually being held to — how fast, how often, and what can't be allowed to degrade badly:
- Playback should start within roughly 2 seconds of a viewer pressing play — a slow start reads as a broken player.
- Assume 1 million new videos uploaded per day, averaging 10 minutes each, with view volume vastly exceeding upload volume — a small fraction of videos account for the large majority of total views.
- Once published, a video's stream must degrade gracefully under changing network conditions rather than stalling outright.
Capacity Estimation
Two of those numbers — 1 million uploads a day, 10 minutes each — are worth turning into real math before we design anything against them.
At 1 million uploads/day averaging 10 minutes each, that's 10 million minutes of raw video per day; at a typical raw upload bitrate around 5Mbps, that works out to roughly 375TB/day of raw uploads alone — well past what any single node stores, and the concrete reason raw video lands in object storage, never a database.
Transcoding multiplies that further: generating the standard delivery ladder of renditions (six rungs, 144p through 1080p — see the table below) adds derived storage on top of the original, and even though each lower-res rendition individually costs far less than the source, the total across all six commonly runs 1.5–2x the original's footprint. That multiplier is the number behind the trade-off you'll see below — transcoding every rendition eagerly for every single upload spends real storage and compute on renditions that, for the large share of videos that get only a handful of views, may never actually be requested.
That 375TB/day also has a throughput shape worth naming: spread evenly across a day, it's roughly 4.3GB/sec (~35Gbps) of sustained ingress — the concrete reason uploads go straight to object storage via a presigned URL instead of routing through an app server, which would need to absorb that bandwidth itself.
Egress dwarfs that. Even a modest 200 million views/day, averaging 5 minutes of playback at a blended ~2Mbps across the rendition ladder, works out to (200,000,000 × 300s × 2Mbps) / 86,400s ≈ 1.4Tbps of average egress bandwidth — roughly 40x the ingress figure above, and the concrete reason CDN-fronted delivery is mandatory rather than a nice-to-have at this scale.
Core Entities
With the storage shape and that multiplier in mind, here's the data actually worth persisting:
type Video = {
id: string;
uploaderId: string;
status: "processing" | "ready";
durationSeconds: number;
createdAt: Date;
};
type Rendition = {
videoId: string;
resolution: string; // e.g. "720p"
bitrateKbps: number;
segmentManifestUrl: string; // HLS/DASH manifest for this rendition
};
API Interface
Two endpoints on top of that shape cover everything we've scoped so far:
POST /videos/upload-url -> { uploadUrl, uploadId } // presigned, direct-to-storage
POST /videos { uploadId, title } -> 201 { videoId, status: "processing" }
GET /videos/:id/manifest -> { manifestUrl } // master playlist listing all renditions
High-Level Design
Now let's put those endpoints, and the pipeline behind them, together into one working design.
From upload to a watchable video
Client uploads raw file
direct to Object Storage via a presigned URL
Transcoding Pipeline
parallel worker fleet, split by chunk
Renditions written to Object Storage
six rungs, each chunked into short segments
CDN
caches segments at the edge, close to viewers
Upload reuses the same presigned, direct-to-storage pattern named in Object Storage — the raw file never transits through an app server. Everything after that split is worth taking apart one piece at a time, starting with the pipeline that turns one upload into a rendition ladder.
Transcoding: turning one upload into a rendition ladder
A raw upload gets split into a handful of multi-minute chunks — a few minutes each, not the whole video — purely so the encode itself can run in parallel across a fleet of workers instead of on one machine start to finish. Each chunk transcodes independently into every rung of the rendition ladder, so a long video's processing latency ends up closer to a function of available worker capacity than of the video's raw length.
Parallel transcoding across a worker fleet
Raw upload
split into ~3-minute chunks
Chunk 1
Worker A — every rung
Chunk 2
Worker B — every rung
Chunk 3
Worker C — every rung
Object Storage
renditions + manifest assembled
A workflow orchestrator tracks which chunk of which video is done, and retries only the failed piece — not the whole video — if a worker dies mid-encode. If you've never touched a video encoder or a workflow orchestrator like this yourself, that's fine; the idea worth carrying into an interview is "stateless workers, each handling one small independent piece, retried independently," not hands-on familiarity with a specific tool.
Once every chunk is transcoded, the pipeline stitches the per-chunk outputs back into one continuous file per rung, then re-chunks that into much shorter segments — a few seconds each — for delivery. This is a genuinely different "segment" from the multi-minute one above, and worth keeping the two straight: the encode-time chunk exists to parallelize processing across workers, while the delivery-time segment exists to make the player's bitrate switch fine-grained later. A concrete rendition ladder, worth having ready if asked:
| Rendition | Resolution | Typical bitrate |
|---|---|---|
| 144p | 256×144 | ~100 Kbps |
| 240p | 426×240 | ~400 Kbps |
| 360p | 640×360 | ~750 Kbps |
| 480p | 854×480 | ~1.2 Mbps |
| 720p | 1280×720 | ~2.5 Mbps |
| 1080p | 1920×1080 | ~4.5 Mbps |
With every rung sitting in storage as short segments, the only piece left is how a viewer's player actually decides which one to request.
Adaptive bitrate streaming: how the player picks a rendition
Player decision loop
Client
fetches the manifest
Measure recent throughput
from the last segment's download time
Pick the highest rung that fits
without draining the playback buffer
Request the next segment
at that rung
Say a viewer's connection is doing about 6 Mbps when playback starts — comfortably above the 1080p rung's ~4.5 Mbps, so that's what the player requests first. A few segments later, their throughput drops to around 1.2 Mbps — a shared Wi-Fi network getting busy, a phone switching off Wi-Fi entirely, it doesn't matter why. The player's next segment request drops straight to the 480p rung, the highest one its own measured throughput can sustain without the playback buffer draining faster than it refills.
Nothing server-side decided this, or was even told about the switch — it's entirely the player, comparing its own numbers against the manifest's rung table. That's what makes this scale for free on the server side: however many viewers are mid-playback at once, none of them cost the server a bitrate decision.
With the pipeline and the delivery mechanism both on the table, here's where the real trade-offs sit.
Trade-offs
Eager vs. lazy (on-demand) transcoding. This is the moving-boxes question from the top, stated precisely: pack every box size for every shipment, or only pack the sizes you already know you'll need, filling in the rest on request?
Eager transcoding — every rung generated immediately on upload — guarantees a video is ready in every quality the instant anyone requests it. The real cost is the multiplier from Capacity Estimation: real compute and storage spent on renditions that, for the large share of videos with only a handful of views, may never be watched at all.
Chosen: a hybrid — eagerly transcode the one or two most commonly requested rungs (covering most viewers on a typical connection), and transcode the rest lazily on first request, caching the result afterward. You get most of eager's instant-availability with a fraction of its wasted compute.
CDN-fronted delivery vs. serving straight from origin storage. Serving segments directly from Object Storage is genuinely simpler — no extra infrastructure, no cache-invalidation story, one fewer system that can be down. For a video with only a handful of views, that simplicity is real: the cost of hitting origin directly is trivial at that volume.
The problem is the view-count skew from Capacity Estimation: a small share of videos account for the large majority of views, and every one of those views hitting origin directly adds up to bandwidth that scales with total watch time, not with video count. A CDN absorbs that by serving repeat requests for the same hot segments from an edge location near the viewer instead of from origin.
Chosen: CDN in front of Object Storage — once an edge location has cached a hot video's segments, origin only has to serve that video once per edge location, no matter how many more times it gets watched from nearby.
Fixed single-bitrate delivery vs. adaptive bitrate streaming. A single fixed stream is simpler to build, but leaves every player stuck buffering whenever available bandwidth drops below what that one stream needs. Chosen: adaptive bitrate streaming — segmenting every rendition so the player can switch between them mid-playback as conditions change, the industry-standard approach for exactly the reason demonstrated above.
Codec choice: H.264 vs. a modern codec like AV1 or VP9. H.264 decodes on essentially every device in the wild, and its encoder is fast and cheap to run at upload volume — the safe, compatible default this lesson's rendition ladder already assumes.
A modern codec can cut the bitrate needed for the same visual quality by roughly 30–50%, which lands directly on the egress math above — real money at that scale. The cost is encoder compute: it can run several times more CPU-hours per video than H.264 to produce.
Chosen: H.264 for every upload by default, with a modern codec added only once a video crosses a view threshold — the same asymmetry as the eager-vs-lazy trade-off above, just spent on codec choice instead of rendition count.
Put these decisions together and here's the shape the design settles into.
Final Design
Final design
Client
CDN
edge-cached segments
API Service
uploads + manifests
Object Storage
raw upload + all renditions
Metadata DB
Video + Rendition rows
Transcoding Workers
triggered on upload, write renditions back up
Every piece here scales on its own axis:
- CDN absorbs the large majority of playback traffic for hot videos; origin only has to serve a segment once per edge location's cache population.
- API Service stays mostly stateless and scales horizontally against manifest and upload traffic — it's barely touched by transcoding at all.
- Transcoding Workers scale against upload volume and average video length, not against playback volume — a video going viral doesn't add a single new transcode job.
- Object Storage and Metadata DB sit underneath both paths as the source of truth everything above can be rebuilt from if it's lost.
With the shape settled, it's worth naming what you'd actually watch once this is running in production.
Operations & Observability
A design that only covers the happy path — upload, transcode, stream — isn't finished; you also want to know it's breaking before a support ticket tells you. Raising this unprompted is one of the clearest signals that separates a senior or staff-level answer from one that stops at "and then it works."
On the transcoding side, watch worker-pool queue depth — a climbing
backlog means new uploads are taking longer to become watchable than the
design assumes, well before any single upload actually times out. Pair
it with chunk failure rate (retries per chunk, not per video) — a
rising rate flags a bad input edge case, like a corrupt upload or an
unsupported codec, across the fleet before it silently piles up as a
growing set of videos stuck in processing.
On the playback side, watch time-to-first-frame first — the literal 2-second playback-start target from Non-Functional Requirements above. A rise here means something upstream of the player loop is slow (manifest fetch, first-segment retrieval, CDN cold-cache on a just-published video), and it's the one metric that catches a broken start even when everything after start plays back perfectly.
Once playback is underway, watch rebuffer rate — if adaptive bitrate streaming is doing its job, rebuffers should stay near zero regardless of a viewer's connection, so a rise usually means the rung ladder has a real gap rather than a broken player. Add CDN cache hit ratio for actively-hot videos — a drop for a video well past its cache-warming window is the earliest sign the CDN-vs-origin trade-off above isn't holding, and origin bandwidth is quietly absorbing traffic it wasn't sized for.
With the metrics in place, here's how this design's depth maps onto where you'd actually land in an interview.
Level Expectations
Wherever you land on this ladder is fine — here's roughly how the bar moves as you go up it.
Mid-level: proposes storing raw video in object storage and transcoding to at least one deliverable format before playback.
Senior: proactively proposes a multi-rendition transcoding ladder plus CDN delivery, names adaptive bitrate streaming by mechanism rather than just by name, and names time-to-first-frame, rebuffer rate, and CDN cache hit ratio as what they'd actually watch in production, not just what they'd build.
Staff: reasons about the eager-vs-lazy transcoding trade-off given real view-count skew, about parallelizing a single video's transcode across many workers so processing latency doesn't scale linearly with video length, and about what a climbing chunk-failure rate or queue depth actually implies for worker-fleet sizing before it becomes a user-visible delay.
With the bar for each level laid out, let's put some of that reasoning to the test with a few follow-ups an interviewer might actually ask.
Follow-Up Questions
Interviewer:A video is 3 hours long. Does transcoding it take roughly 3x as long as a 1-hour video, and does that matter for this design?
Not if the pipeline splits the source into independently-encodable chunks processed in parallel across many workers — the same multi-minute-chunk shape from Transcoding above. Processing latency becomes a function of available worker capacity, not raw video length, the same parallelization the delivery side already relies on for adaptive streaming, applied here to processing instead of playback.
Interviewer:How does the player actually decide when to switch renditions mid-playback?
It measures its own recent segment-download throughput, compares that against the rendition ladder's bitrates, and requests its next segment from whichever rung fits without risking its playback buffer running dry — entirely a client-side decision, the same mechanism worked through above. Nothing server-side needs to know which rung any given viewer is currently watching.
Interviewer:A bug is discovered in the transcoding pipeline after millions of videos have already been processed with it. What does fixing it actually involve?
A large but bounded backfill: re-transcoding from each video's retained raw original, not asking users to re-upload anything — which is exactly why the raw original stays in object storage rather than getting discarded after the first transcode. It's a background reprocessing job over existing data, using the same chunked worker-fleet mechanism already built for the original transcode, not something that touches the live upload or playback request path at all.
Interviewer:A video goes viral overnight — tens of millions of views in a few hours. What breaks if every playback does a direct database increment to the view count?
That single row becomes a lock-contention hotspot — every concurrent viewer serializes on writing the same counter, right as the database becomes the bottleneck. The fix follows the same shape as everything else here: stop writing synchronously on the request path. Batch view events and flush aggregated counts to the Video row on an interval instead, trading a few seconds of staleness for a write path that never contends.
Those follow-ups show how far this design's core mechanisms stretch — now it's your turn to push on one yourself.
Try It Yourself
Try it yourself
Sketch what changes for live streaming instead of pre-recorded video. There's no already-stored raw original to transcode segments from ahead of time — what does that do to the "transcode once, cache renditions, serve from CDN" shape this design relies on?