Design Netflix

Design Netflix step by step: licensed catalog, CDN and Open Connect, ABR playback, licenses, watch resume, Home rows, failures, and the reasoning behind every major decision.

Page content

Bob opens Netflix on a Friday night. He does not upload a file. He picks an episode from a row that already knew he likes crime shows, hits play, and the first segment should start in about a second — even if half his city is watching the same title.

That is not YouTube. YouTube’s hard parts are anyone uploading, transcoding unknown files, and viral view counters. Netflix’s hard parts are a licensed catalog, prime-time playback of the same bytes, and which title belongs on the row.

The main design question is:

How do we start playback fast from a nearby cache, keep the catalog and licenses correct, and rank Home without treating every member as a creator?

We will start with metadata and a player that never talks to the API for video bytes. CDN, licenses, progress, and recommendation rows appear when a simpler path fails.

1. Clarify the problem

“Design Netflix” includes live sports, games, ads, and a global Open Connect fleet. That is too broad for one interview.

I would ask:

  • Streaming only, or offline downloads?
  • Live events in scope?
  • How many titles, languages, and countries?
  • Must we enforce DRM / license checks before segments play?
  • Are recommendations in scope, or only “continue watching”?
  • What concurrent streams at prime time?

If the interviewer gives no extra constraints, I would state:

Product              Browse, play, pause/resume, history, Home rows
Not in scope         Live, user upload, comments, ads
Catalog              Studio titles, seasons, episodes, many bitrates
CDN                  Edge caches + ISP appliances (Open Connect idea)
License              Short-lived token required to fetch segments
Downloads            Same files, offline license; mentioned, not deep
Profiles             One account, several profiles (maturity, language)
Deployment           Metadata one region first; CDN is global

If they say “video streaming” without a product, ask UGC or catalog. That single question picks this design or YouTube.

2. Functional requirements

The system must:

  1. Browse title, season, and episode metadata.
  2. Search titles.
  3. Start playback of an episode with adaptive bitrate.
  4. Resume from the last watch position.
  5. Keep watch history and “continue watching.”
  6. Show a personalized Home made of rows of titles.
  7. Enforce country availability and maturity on play, not only on the row.

The first version does not include:

  • members uploading videos;
  • a comments product;
  • live sports;
  • a real DRM vendor integration (we still model a license service);
  • encoding the studio mezzanine in the request path (that is a batch pipeline).

3. Non-functional requirements

RequirementTarget
Time-to-first-frameabout 1 s when the title is warm at the edge
Prime-timeMillions of concurrent viewers of the same episode
Catalog readHigh QPS, rare writes
ResumeLast-write-wins; a jump of a few seconds is acceptable
LicenseNo segment without a valid token
AvailabilityPlay continues from CDN if catalog DB is briefly down
Catalog metadata     source of truth (CMS / DB)
Bytes                object storage + CDN
Watch position       per profile, frequent small writes
Home rows            derived, stale-OK for minutes
License              short-lived, must be checkable

Edge cases to keep in mind

  • Friday-night episode is a hot object at every POP.
  • License service is down; the player still has 20 seconds of buffer.
  • Title leaves the country catalog while Bob is mid-episode.
  • Two devices in one profile update progress at once.
  • Home cache still shows a title that expired at midnight.
  • Origin is slammed because a new drop was not pre-positioned.
  • A child profile requests a maturity-restricted title via a guessed URL.

4. Estimate the scale

These are planning values, not Netflix’s 10-K.

Paying members                   50 million
Profiles                         ~2 per account → 100 million
Prime-time concurrent streams    5 million
Average bitrate                  5 Mbps
Catalog                          10,000 titles, many episodes
Home loads                       2 / member / day
Search                           0.5 / member / day
Progress updates                 every 15 s while playing

Egress if 5 million streams run at 5 Mbps:

5,000,000 × 5,000,000 bits/s = 25 Tbps
25 Tbps / 8 ≈ 3 TB/s

The API will never carry that. CDN and ISP caches exist because of this number.

Playback starts (manifest + license), not segment QPS:

5,000,000 starts if everyone pressed play in one minute
is a stampede; normally starts are far fewer than segment fetches

Progress writes at prime time:

5,000,000 / 15 ≈ 330,000 writes/s

That is why progress is not UPDATE episodes. It is its own store.

Home:

50e6 × 2 / 86,400 ≈ 1,160 average QPS
20× peak           ≈ 23,000 QPS

Catalog writes are CMS publishes: tens per hour, not per second.

Bottlenecks, in order:

1. CDN / ISP egress and origin on a cold hit
2. License service at a play stampede
3. Progress store write rate
4. Home ranker / cache
5. Catalog DB — only if you put bytes or progress on it

5. What are we storing?

Title / season / episode     metadata, artwork keys
Availability                 country, window, maturity
Playback asset               renditions, master.m3u8 key
Profile                      language, maturity, pin
Watch position               profile + episode + offset
My list                      profile + title
Home row cache               profile + row id + title ids

Bytes live elsewhere:

origin://catalog/{title}/{episode}/source
origin://catalog/{title}/{episode}/1080p/segN.ts
origin://catalog/{title}/{episode}/master.m3u8
Source of truth     catalog DB, license decisions, progress store
Derived             search index, Home rows, CDN copies
Cache               title:{id}, home:{profile}
Ephemeral           playback session, license TTL

6. APIs

Keep JSON on the API. Keep segments on the CDN.

Title detail

GET /v1/catalog/titles/tt_crime
Authorization: Bearer <profile>
HTTP 200 OK

{
  "id": "tt_crime",
  "name": "Night Shift",
  "seasons": [
    { "id": "s1", "episodes": [{ "id": "ep_9", "duration_seconds": 3120 }] }
  ],
  "artwork_url": "https://cdn.example/art/tt_crime.jpg"
}

Start playback

POST /v1/playback/ep_9/session
Authorization: Bearer <profile>
HTTP 200 OK

{
  "session_id": "ps_41",
  "manifest_url": "https://cdn.del.example/ep_9/master.m3u8?exp=...",
  "license_url": "https://license.example/v1/licenses/ps_41",
  "resume_offset_seconds": 842,
  "expires_at": "2026-08-22T05:20:00Z"
}

If the title is not available in Bob’s country or the profile’s maturity is too low:

HTTP 403

{
  "error": {
    "code": "TITLE_NOT_AVAILABLE",
    "message": "This title is not available for this profile"
  }
}

Progress and Home

POST /v1/playback/ep_9/progress

{
  "session_id": "ps_41",
  "offset_seconds": 900
}
GET /v1/home
GET /v1/catalog/search?q=night&cursor=...

Home returns title ids plus artwork URLs, not video bytes.

7. Basic data model

titles
  id, name, type MOVIE|SHOW, created_at

seasons
  id, title_id, number

episodes
  id, season_id, number, duration_seconds

title_availability
  title_id, country, starts_at, ends_at, max_maturity

assets
  episode_id, kind MANIFEST|RENDITION|ART
  resolution, object_key

profiles
  id, account_id, maturity, language

watch_positions
  profile_id, episode_id, offset_seconds, updated_at
  PK (profile_id, episode_id)

my_list
  profile_id, title_id

playback_sessions
  id, profile_id, episode_id, issued_at, expires_at

Indexes:

episodes (season_id, number)
title_availability (country, title_id)
watch_positions (profile_id, updated_at)     continue watching

Do not put view_count on titles as a hot UPDATE. Popularity for ranking is a batch job.

8. Start with metadata and a dumb player URL

A first version: PostgreSQL catalog, one origin bucket, GET /play/{id} returns a single MP4 URL.

That teaches authorization. It fails at prime time: every TV hits origin, there is no ABR, there is no license, and Home is ORDER BY name.

We keep the catalog and evolve playback.

9. Catalog is a CMS, not a firehose

Titles change on a schedule. A publish is:

CMS editor
BEGIN
  upsert title / episode / availability
  insert outbox CatalogPublished
COMMIT
Indexer → search
Cache invalidate title:{id}
CDN purge if a window ended

Writes are rare. You can cache title:{id} for minutes and invalidate on publish. That is the opposite of Instagram, where every user writes.

When a license window ends at midnight UTC, Home cache and search must drop the title, and new playback sessions must 403. A player already in episode 9 is a product choice: finish the session or cut at the next license refresh. I would cut at license expiry so we do not stream after the contract ends.

10. Why the API must not stream bytes

Without CDN                         With CDN

TV ──► API pod ──► disk             TV ──► nearby cache ──► (miss) origin
       5 Mbps × N TVs                        hit ratio ~99% on Friday

25 Tbps through Go pods is not an architecture. The playback API only:

1. Authenticate profile
2. Check availability + maturity
3. Create playback session
4. Return signed manifest URL + license URL

The player then talks to the CDN. Same control-plane / data-plane split as YouTube, with a known catalog so we can pre-position.

High-level, before we fill in ABR and licenses:

                         ┌──────────────┐
                         │  TV / app    │
                         └───┬──────┬───┘
                             │      │
                   JSON      │      │  segments / artwork
                             │      │
                      ┌──────▼──┐   │
                      │   API   │   │
                      │ catalog │   │
                      │ home    │   │
                      │ session │   │
                      └───┬─────┘   │
                          │         │
              ┌───────────┼─────────┼────────────┐
              ▼           ▼         ▼            ▼
         Catalog DB    Progress   License    CDN / ISP
         + title cache  store     service    + origin

The left column is the control plane. The right column is the data plane. Mixing them is the first failure mode.

11. Adaptive bitrate

Encode offline, before release week:

episode ep_9
  master.m3u8
    ├─ 4K/index.m3u8
    ├─ 1080p/index.m3u8
    ├─ 720p/index.m3u8
    ├─ 480p/index.m3u8
    └─ 360p/index.m3u8

The player picks a ladder from bandwidth and buffer. The API does not implement ABR. A train passenger drops to 360p without another API call.

YouTube does this after an unpredictable upload. Netflix does it in a batch transcoder when the studio delivers a mezzanine. Interviewers like that contrast.

12. CDN and Open Connect

The player never asks the API for a .ts segment. After the session exists, the path is:

                    ┌──────────── ISP / city POP ────────────┐
                    │  Open Connect appliance                │
 TV / app ─────────►│  already has this week's top titles    │
                    └────────────┬───────────────────────────┘
                                 │ miss
                           Regional cache
                                 │ miss
                    Origin shield (coalesce)
                           Origin object storage

Pre-position: before Friday’s drop, push the new episode to appliances and regional caches. YouTube cannot do this for a random 2 GB upload. Netflix can, because the release calendar is known.

Origin shield: if a title was not pushed, the first misses should coalesce on a shield so origin sees one pull, not 50,000.

Hot object: the same ep_9/720p/seg0001.ts is requested by a city. That is a CDN problem, not a Redis problem. Do not put segment bytes in Redis.

A local CDN interface in a code project returns a path. In the interview, the box is “edge + ISP cache + origin.”

13. Licenses

A guessed manifest URL must not play a paywalled title.

POST /playback/ep_9/session
  → session ps_41, expiry 15–30 min
  → cookie or token bound to profile + episode + IP/device (product choice)

Player GET license_url
  → license service checks session
  → returns decryption keys / widevine-style payload

Segment requests from the CDN can require the same token. If the license service is down:

New play     fail closed (403/503)
In-flight    buffer may last tens of seconds; next key rotation fails

Do not skip licenses to “keep it simple” if the interviewer mentioned DRM. A signed, short-lived URL is the minimum credible stand-in.

14. Watch position

Playing
  every 15s or on pause/seek
  PUT watch_positions (profile, episode) = offset
profile p_bob, episode ep_9
  offset 900
  updated_at 05:04:12

Two devices: last write wins. Bob’s TV and phone may disagree by one heartbeat. That is acceptable. A bank ledger is not the metaphor.

Storage: Redis for the hot offset + async flush, or a wide-column table keyed by profile_id. Do not update episodes. Do not write progress into the catalog transaction.

Continue watching is:

SELECT episode_id, offset
FROM watch_positions
WHERE profile_id = p_bob
  AND offset > 0
  AND offset < duration - 30s
ORDER BY updated_at DESC
LIMIT 20

15. Home is rows, not fan-out

A new episode is not written into 50 million inboxes. Everyone can read the same “New this week” list.

GET /home
  ├─ Continue watching      progress store
  ├─ Because you watched X  precomputed similar titles
  ├─ Trending in IN         country popularity job
  └─ New releases           CMS list
  Filter country + maturity
  Cache home:{profile}  2–10 min

Candidate generation + rank, same shape as YouTube Home, on a small catalog. Offline batch builds most rows. Online rank can shuffle and insert continue-watching first.

A cached Home that still contains an expired title must be filtered again at serve time. Cache is not authorization.

Ten thousand titles: OpenSearch or even a prefix index is enough. Publish from the catalog outbox. Rank with popularity and a personalization boost. This is not hotel search. Do not invent geo and live prices.

If search is down, browse and play still work.

17. Consistency

Strong
  availability check at session create
  license issue

Last-write-wins
  watch offset

Eventual
  Home rows, search, CDN copies of new files

Unacceptable
  playing after a hard window end (if that is the contract)
  child profile playing an 18+ title

18. Failure scenarios

FailureBehavior
Catalog DB downServe cached title metadata; no CMS publish
Object storage / origin downWarm titles still play from CDN; cold titles fail
CDN miss stormShield + admission; pre-position next time
License downNo new sessions; in-flight buffer only
Progress store downPlay works; resume may jump to 0
Kafka / indexer downSearch stale; play unaffected
Home cache staleRe-filter availability on read
Region mismatch403 at session create

19. Observability, security, scale

Log request_id, profile_id, episode_id, session_id, country, cdn_pop. Do not log license material.

Metrics that matter:

time_to_first_frame
cdn_hit_ratio
origin_bytes
license_latency / error
progress_write_qps
home_latency

Security: every play path re-checks maturity and country. Tokens expire. Rate-limit session creates per profile (rate limiter).

Scale:

10k members     one origin, one API, SQL progress
1M members      CDN, Redis title cache, progress store
50M members     ISP caches, pre-position, shield, Home batch jobs
100M+           multi-region metadata, anycast CDN, isolated license tier

Partition progress by profile_id. Partition catalog by title_id. Never partition video bytes through the API.

20. Where logic lives

internal/catalog      titles, availability
internal/playback     session, license client, manifest URL
internal/progress     offset upsert
internal/home         row assembly, filter
internal/search
internal/cms          publish + outbox

Handlers do not choose bitrates. The player does. CMS workers do not serve play.

21. How this differs from YouTube

YouTubeNetflix
Who uploadsAnyoneStudios / CMS
Hot problemUpload, transcode, view countersPrime-time CDN, catalog, rank
Pre-positionHardRelease calendar
View countOn the watch pageBatch popularity
CommentsCoreUsually none
HomeCandidates from a firehoseRows on a fixed catalog

22. Final architecture

  TV / mobile / browser
           │  JSON: catalog, home, session, progress
           │  bytes: never through the API
     ┌─────────────┐
     │ API gateway │
     └──────┬──────┘
     ┌──────┴──────────────────────────────────┐
     │                                         │
     ▼                                         ▼
 Catalog / Home / Search                 Playback Service
     │                                         │
     ├── metadata DB                           ├── check availability
     ├── Redis title:{id}                      ├── issue session
     ├── search index                          ├── license service
     └── home:{profile} cache                  └── signed manifest URL
 CMS publish ──► outbox ──► indexer / cache bust / CDN purge

  Player
  License Service ──► keys for this session
  CDN / ISP Open Connect ──miss──► origin shield ──► object storage

Play:

Bob → POST /playback/ep_9/session
        authorize + maturity + country
        create session
        return manifest + license URL + resume offset
Bob's player → license
             → CDN master.m3u8
             → segments (ABR)
             → POST progress every 15s

Home:

Bob → GET /home
        continue watching from progress
        + precomputed rows
        filter availability
        return title ids + artwork CDN URLs

23. Interview-ready summary

How to walk through in 10–15 minutes

0–2 min. Catalog vs UGC. Friday-night egress.
2–5 min. APIs: session, not bytes. Availability and maturity.
5–9 min. ABR ladder, Open Connect, pre-position, origin shield, license.
9–12 min. Progress store, Home rows, search.
12–15 min. Failures, child-profile deep link, YouTube contrast.

Key decisions to remember

  1. Ask catalog vs UGC in the first minute.
  2. The API never streams video bytes.
  3. ABR is many renditions plus master.m3u8; the player adapts.
  4. License / signed URL on every new session; fail closed.
  5. Progress is a per-profile store, not the episode row.
  6. Home is rows and a ranker, not fan-out to 50 million inboxes.
  7. Pre-position known drops; Open Connect is the egress story.
  8. Re-filter country and maturity on play and on Home serve.
  9. CDN hit ratio and time-to-first-frame are the metrics.
  10. Origin shield protects a cold title; Redis does not store segments.

Likely interviewer follow-up questions

  • How is this different from YouTube?
  • Why 25 Tbps is not an API problem?
  • What is Open Connect in one sentence?
  • What happens if the license service dies mid-episode?
  • How do you stop a child profile from playing an 18+ title via a deep link?
  • Where do you store “minute 14:02”?
  • Why not fan-out a new episode to every member?
  • How do you survive the Friday drop if caches are cold?
  • 301/CDN vs origin — who is source of truth for the file?
  • How would downloads work?

Senior-level points that differentiate the answer

  • Separate control plane (session, license) from data plane (CDN).
  • Pre-position is a Netflix-shaped advantage; say why YouTube cannot copy it for UGC.
  • Filter authorization twice: Home cache and play.
  • Treat progress as high-QPS last-write-wins, not as catalog data.
  • Quote egress math once, then refuse to put bytes on the API.
  • Origin shield and hit ratio before “more Kafka.”

A 1–2 minute verbal answer

I would design Netflix as a catalog streamer, not a UGC site. At 50 million members and 5 million prime-time streams at 5 Mbps, egress is on the order of 25 Tbps, so the API only issues a playback session: availability, maturity, a short-lived license, and a CDN manifest URL. The player does adaptive bitrate from master.m3u8. Popular titles are pre-positioned on ISP caches; origin is shielded on a miss.

Catalog metadata is a low-write CMS with heavy cache. Watch position is a (profile, episode) key updated every few seconds, not an update on the episode row. Home is continue-watching plus precomputed recommendation rows, filtered again at serve time. Search is a small index. If license is down, new plays fail closed. If the catalog DB is down, cached metadata and CDN bytes can still serve a warm title.

For the YouTube contrast, see Design YouTube. For the broader framework, see the System Design Interview Complete Guide.