YeetCode
System Design · Advanced Designs

Design Netflix: Streaming, OpenConnect, and Recommendations at Scale

How to design Netflix in a system design interview — ABR streaming, OpenConnect CDN, DRM, capacity math for 250M users, and the recommendations serving path.

11 min readBy Bhavesh Singh
netflix system designvideo streamingadaptive bitrateopenconnect cdndrmrecommendations

This article has an interactive companion

Don't just read it — step through it interactively, one state change at a time.

Practice this topic on YeetCode

Ask someone to "design Netflix" and they usually start drawing microservices. That's the wrong first move. Netflix is a bandwidth problem wearing a software costume: at peak, it pushes hundreds of terabits per second of video into homes worldwide, and almost every interesting design decision falls out of that one number.

The trick is to separate the two halves of the system. There's a fat, boring, static data plane — pre-encoded video segments served from caches near the user — and a thin, clever control plane that decides which cache, which bitrate, whether you're allowed to watch, and what to recommend next. Get that split right and the rest of the interview is detail.

This walks the standard interview rubric — requirements, capacity math, architecture, APIs, data model, then deep dives — using real Netflix numbers so you can defend every claim under follow-up questions.

Functional and non-functional requirements

Start by pinning down what a caller can actually ask for. For a streaming service the core functions are: browse and search a catalog, start playback of a title, adapt video quality to the network mid-stream, resume from where you left off on any device, and get a personalized home page. Uploading and encoding source masters is an offline function, not part of the play-time hot path.

The non-functional bounds are where the design lives:

RequirementTargetWhy it drives design
Startup latencyUnder ~2s to first frameForces license + manifest fetch in parallel, pre-cached segments
Rebuffer ratioAs close to 0% as possibleForces buffer-aware ABR, not naive bandwidth guessing
AvailabilityPlayback survives control-plane outagesClient caches a manifest + OCA list, keeps playing
Egress costMinimize transit spendForces caches inside ISP networks, not commercial CDN
Catalog durabilityNever lose an encoded masterCheap: object storage with replication

Notice that consistency barely appears. Nobody cares if your "Continue Watching" bookmark is a few seconds stale. That relaxation is what lets the write path be cheap and fast.

Capacity estimation: why the numbers force the architecture

Capacity math isn't busywork here — it's the argument for the entire CDN strategy. Take concrete figures: 250M subscribers, 2 hours of average daily watch time, an average encoded bitrate of 4 Mbps, and a peak-to-average ratio of 3x.

First, average concurrent streams. If each user watches 2 of every 24 hours, the fraction streaming at any instant is 2/24:

text
concurrent streams = 250M × (2 / 24) ≈ 20.8M streams

At 4 Mbps each, average egress is:

text
avg egress = 20.8M × 4 Mbps ≈ 83 Tbps peak egress = 83 Tbps × 3 ≈ 250 Tbps

And the daily volume served:

text
daily bytes = 250M × 2h × 3600 s × 4 Mb/s ÷ 8 ≈ 900 PB/day

Sit with 250 Tbps for a second. No commercial CDN will sell you a quarter-petabit-per-second of egress at a price that survives a subscription business — transit alone would dwarf every other cost. That single number is why OpenConnect exists. It's also why per-title encoding and the AV1 rollout matter financially: shaving even 20% off the average bitrate saves roughly 50 Tbps at peak and about 180 PB every day.

High-level architecture: the data plane / control plane split

The design's spine is the separation the capacity math demands.

The control plane runs in the cloud (AWS, for Netflix). It handles auth, billing, the catalog and search, viewing history, DRM licensing, recommendations, and — critically — steering: when you press play, it returns a ranked list of caches to stream from. It's request-driven, stateful, and can tolerate being heavy because it's off the byte-moving hot path.

The data plane is Open Connect: fleets of Open Connect Appliances (OCAs) — essentially fat storage boxes — placed inside ISP networks and at internet exchange points. They hold pre-encoded video segments as ordinary static files and serve them over HTTPS. They're stateless and dumb by design, which is exactly what makes them cacheable and cheap to replicate.

Content lands on OCAs through nightly fill windows. Netflix predicts, per region, what will be popular tomorrow and pushes those encodings out during off-peak hours over cheap bandwidth. So when your evening viewing spikes, the bytes are already sitting one hop away inside your ISP, and peak-hour transit is near zero.

At play time the client asks the control plane for a title; the control plane returns a ranked OCA list computed from BGP/geo proximity, each OCA's health and current load, and whether that title is already resident on it. The client opens segment requests to the top choice and, on any error, fails over down the list. The result: playback keeps working even if the control plane hiccups, because the client already holds the manifest and the OCA list.

What is adaptive bitrate streaming?

Video isn't shipped as one file. Each title is encoded into a ladder of renditions — say 240p up to 4K — and every rendition is chopped into 2–10 second segments listed in a manifest (HLS or DASH). The client downloads segments one at a time and, between each, decides which rendition to fetch next.

The naive approach is to estimate raw throughput and grab the highest bitrate that "fits." It rebuffers constantly. On variable networks (mobile, congested Wi-Fi) a throughput estimate swings wildly, so the player oscillates and, worse, blows through its buffer right when the network dips, causing a stall — the one thing users hate most.

Modern ABR is buffer-aware. It blends three signals: how many seconds of video are already buffered, a smoothed (EWMA) throughput estimate, and the predicted cost of a rebuffer versus a quality drop. When the buffer is healthy it's willing to push quality up; when the buffer drains it drops rendition preemptively rather than risking a stall. Netflix pairs this with per-title encoding: a simple cartoon gets a leaner bitrate ladder than a grainy action film, so bits aren't wasted encoding easy content at blockbuster bitrates.

API design

The play-time APIs are deliberately small. A sketch:

text
GET /titles/{id}/playback -> { manifestUrl, ocaCandidates[], sessionId } POST /license -> body: { cdmChallenge, sessionId } returns wrapped keys POST /heartbeat -> body: { profileId, titleId, positionMs, deviceId, ts } GET /home?profileId=... -> { rows: [{ title, items[] }, ...] }

GET .../playback is the steering call: it hands back the manifest plus the ranked OCA list. Segment GETs then go straight to an OCA and never touch the control plane. POST /license runs in parallel with the manifest fetch to hide its round trip. POST /heartbeat is fire-and-forget from the client's perspective, which is why the bookmark store can be eventually consistent.

Data model

Three stores with very different shapes:

  • Catalog metadata — relational or document store, read-heavy, aggressively cached at the edge. Titles, seasons, episodes, artwork, encoding manifests.
  • Bookmarks (Continue Watching) — a low-latency KV store (Cassandra or DynamoDB) keyed by (userId, profileId), with a per-profile secondary index sorted by lastWatchedAt so the home row is one indexed read.
  • Recommendation candidates — precomputed per-user row bundles and embeddings written by offline pipelines into an in-memory store (EVCache/Memcached) for millisecond reads.

The pattern: match each store to its dominant query. A point lookup wants a stable partition key; a "most recent" feed wants a sort key; a latency-critical read wants everything precomputed and cached.

Deep dive: DRM without killing startup latency

Premium 4K HDR titles need real content protection. The player fetches the manifest, then sends a CDM challenge (Widevine L1, PlayReady, or FairPlay) to a license server. The server checks entitlement and device security level — UHD requires an L1 hardware TEE — and returns content keys wrapped to that specific CDM. Keys are short-lived and often rotated per segment.

The latency trap is doing this serially: manifest, then license, then first segment. Instead, issue the license request proactively during startup, in parallel with the manifest fetch, and cache the response client-side (which also enables offline playback).

The failure modes are subtle: a revoked CDM, client clock skew that makes a license look expired, or a key-rotation race during a rendition switch. The right policy is graceful degradation — fall back to a lower security-level rendition like 1080p SDR rather than throwing a hard playback error. A slightly softer picture beats a spinner.

Deep dive: Continue Watching across devices

Clients emit playback heartbeats — position, title, device — every 10–30 seconds to the bookmark service. Writes use last-writer-wins keyed on a monotonic client timestamp, which tolerates clock skew and the reality that heartbeats arrive out of order. There's no coordination and no locking; the write is a single fast KV upsert.

For a sub-second experience when you pause on your phone and pick up on the TV, push updates through a pub/sub channel — Kafka fanning out to WebSocket connections held by active devices — so the other device updates its row in place instead of polling. The home-page "Continue Watching" row is then a single read against the lastWatchedAt secondary index, with title metadata denormalized from cache so nothing blocks on the catalog service.

Deep dive: recommendations under a 200ms budget

The home page personalizes 40-plus rows, and the p99 budget for rendering it is around 200ms. You cannot run heavyweight ranking models inline at that latency.

The split is offline versus online. Offline pipelines (Spark/Flink) train the ranking and candidate-generation models and write each user's precomputed row candidates and embeddings into a fast store. The online service does almost nothing expensive: it fetches the user's candidate bundle, layers on a few real-time features (recent plays, device, time of day), and runs a light reranker per row. Deep models never run in the request path.

Two more safeguards keep p99 honest: request hedging to EVCache replicas so one slow node can't stall the page, and a fallback — if personalization misses its SLA, serve a global "Popular in your country" row so the page always renders something. A generic home page beats a blank one.

Why pre-encode instead of transcoding at the edge?

It's tempting to imagine transcoding on demand at the OCA. It's a trap. Transcoding is CPU/GPU-expensive and has nondeterministic latency, so per-request encoding would wreck startup time and cost. Worse, it destroys caching: every request would produce a unique byte stream, so cache hit rates collapse. Pre-encoding a fixed ladder makes every segment a static, cacheable object, which is what enables 99%+ hit rates at the edge. The one-time encode cost is amortized across billions of views per title — an easy trade.

How this shows up in interviews

Interviewers rarely want the full diagram; they want to see whether you reason from the numbers. Lead with the capacity math, because it's the thing that justifies OpenConnect and pre-encoding — candidates who skip it can't defend "why not just use a CDN." Expect follow-ups on the data-plane/control-plane split, why bookmarks can be eventually consistent, how ABR avoids rebuffering, and how the recommendation path hits its latency budget.

The strongest move is to trace one press of "play" end to end: steering call returns manifest plus OCA list, license fetched in parallel, segment GETs hit the nearest OCA, ABR adjusts rendition per segment, heartbeats trickle back for Continue Watching. Then name the one trade-off you'd revisit at 10x load.

You can explore this design interactively on YeetCode to reinforce the flow.

Further learning

FAQ

Why did Netflix build OpenConnect instead of using a commercial CDN?

The capacity math answers it. At 250M subscribers averaging 2 hours a day at 4 Mbps with a 3x peak factor, peak egress is roughly 250 Tbps and daily volume near 900 PB. No commercial CDN sells a quarter-petabit-per-second of egress at a price a subscription business can absorb — transit would dominate cost of goods sold. By placing Open Connect Appliances inside ISP networks and pre-filling them nightly with predicted-popular content, Netflix serves peak traffic from one hop away at near-zero transit cost.

How does adaptive bitrate streaming avoid buffering?

Each title is encoded into a ladder of bitrates, each split into short segments listed in a manifest. Rather than picking quality purely from a throughput guess — which oscillates and stalls on variable networks — modern ABR is buffer-aware. It blends buffered seconds, a smoothed throughput estimate, and the predicted cost of a rebuffer versus a quality drop, so it lowers rendition preemptively when the buffer drains instead of stalling. Per-title encoding further tunes each ladder so simple content isn't wastefully encoded at high bitrates.

How do you keep the home page fast while personalizing dozens of rows?

Split work into offline and online. Offline pipelines train models and precompute each user's row candidates and embeddings into a fast in-memory store. The online request path stays feature-light: fetch the precomputed bundle, add a few real-time signals, and run a cheap per-row reranker — no heavy models inline. Request hedging to cache replicas prevents one slow node from blowing the p99, and if personalization misses its SLA, a global "popular in your country" row renders so the page is never blank.

How does Continue Watching stay consistent across devices?

Clients send playback heartbeats every 10–30 seconds to a low-latency KV store keyed by user and profile. Writes use last-writer-wins on a monotonic client timestamp, which tolerates clock skew and out-of-order arrivals without any locking. For near-instant cross-device updates, changes are pushed over a pub/sub channel (Kafka to WebSocket fanout) so an active device updates its row in place. Strong consistency isn't needed here — a bookmark that's a few seconds stale is perfectly acceptable, which is what keeps the write path cheap.

Why not transcode video on the fly at the edge?

Transcoding is CPU/GPU-heavy with unpredictable latency, so encoding per request would ruin startup time and cost. It also breaks caching entirely: every request would yield a unique byte stream, collapsing hit rates. Pre-encoding a fixed ladder turns each segment into a static, cacheable object, which is what enables 99%+ edge cache hits and predictable quality. The one-time encode cost is amortized across billions of views, making it far cheaper than repeated on-demand work.

Make it stick: run this one yourself

Don't just read it — step through it interactively, one state change at a time.

Practice this topic on YeetCode