Design Netflix: A System Design Deep Dive
Model Netflix-scale traffic and build a benchmark-driven streaming backend.
Introduction
30 Second Summary
Every time you press play on a streaming service, your request travels through caches, API gateways, and content delivery networks before a single frame reaches your screen. The architecture behind that seamless experience is one of the most asked-about topics in system design interviews.
In this project, you will build a Netflix-like backend piece by piece, with every architectural decision backed by numbers and observable behavior you collect yourself.
What You'll Build
When you're done, you can walk an interviewer through Netflix's full architecture from first principles, demonstrating each component with a running system where you measure latencies, observe cache behavior, and prove why each layer exists.
By the end of this project, you'll have:
- A capacity estimation model that models 300M+ subscriber traffic, showing you exactly why Netflix's control plane and data plane must be separate systems.
- A catalog service where you benchmark real latency numbers before and after adding Redis caching, proving a 10x speedup that justifies Netflix's EVCache pattern.
- A working API gateway, playback service, and two-tier CDN where you observe X-Cache-Status headers flip from MISS to HIT, simulating Netflix's Open Connect edge caching.
- Secret Mission: Build a two-stage recommendation engine with candidate generation and Jaccard similarity ranking, mirroring Netflix's ML pipeline architecture.
Are there any prerequisites?
This project assumes familiarity with Docker, backend development, and REST APIs. You should have Docker Desktop installed before starting.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters.
This project starts where system design interviews start: with the numbers that justify every architectural decision. Understanding scale before writing code is exactly what separates a strong candidate from someone who jumps straight to drawing boxes on a whiteboard.
Set Up and Estimate Netflix-Scale Capacity
System design interviews start with the same question: how big is this thing? Before drawing a single box on a whiteboard, you need to know how much traffic the system handles and where the bottlenecks live.
In this step, you will install your tools, scaffold the project, and run a capacity estimation script that models 300 million Netflix subscribers. The numbers it produces will justify every architectural choice you make in the rest of the project.
In this step, get ready to:
- Install FFmpeg and create the project directory structure.
- Write and run a capacity estimation script that quantifies Netflix-scale traffic.
- Encode sample video into three HLS quality levels for the CDN demo.
Install FFmpeg and create the project structure
You need FFmpeg to encode sample video into the HLS segments your CDN will serve later. You also need a folder for each service in the Netflix architecture.
- Check if FFmpeg is already installed by running this command:
ffmpeg -version
✔️ I see a version number
FFmpeg is ready. Move on to creating your project directory.
ⓧ Command not found
- Install FFmpeg via Homebrew by running this command:
brew install ffmpeg
What does this command do?
Homebrew downloads and installs the ffmpeg binary along with its encoding libraries (H.264, AAC, and more). You will use it to generate HLS video segments for the CDN demo.
Still seeing command not found?
Make sure Homebrew itself is installed. If brew is not recognized, visit brew.sh to install it first, then retry.
Help me install FFmpeg on my machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create the project folder with all service subdirectories by running these commands:
mkdir -p netflix-system-design/catalog-service \
netflix-system-design/playback-service \
netflix-system-design/cdn/origin/videos/sample \
netflix-system-design/cdn/edge \
netflix-system-design/gateway \
netflix-system-design/recommendation-service
cd netflix-system-design
- Confirm the folders exist by running:
ls
You should see catalog-service, cdn, gateway, playback-service, and recommendation-service listed in the output.
Missing folders?
Make sure you ran the full mkdir -p command as a single block. The -p flag creates parent directories automatically.
Help me create the project directory structure.
Create the Docker Compose file
The docker-compose.yml file defines every service in your Netflix backend. You are creating the full blueprint now so you can bring up individual services as you build them in later steps.
- Create a file called docker-compose.yml in the root of your netflix-system-design folder, then paste the following code for the Redis, catalog, and playback services:
services:
redis:
image: redis:7-alpine
ports:
- "6379:6379"
catalog-service:
build: ./catalog-service
ports:
- "8001:8001"
depends_on:
- redis
environment:
- REDIS_HOST=redis
playback-service:
build: ./playback-service
ports:
- "8002:8002"
What does this code do?
Each top-level key under services defines a container that Docker Compose will build and run. redis uses a pre-built Alpine image. catalog-service and playback-service will be built from Dockerfiles you write in later steps.
The depends_on key ensures Redis starts before the catalog service. The REDIS_HOST=redis environment variable tells the catalog service to connect to the Redis container by its service name.
- Now add the CDN and gateway services below the playback service in the same file:
cdn-origin:
image: nginx:alpine
ports:
- "8003:8003"
volumes:
- ./cdn/origin/nginx.conf:/etc/nginx/conf.d/default.conf
- ./cdn/origin/videos:/usr/share/nginx/html/videos
cdn-edge:
image: nginx:alpine
ports:
- "8004:8004"
volumes:
- ./cdn/edge/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- cdn-origin
gateway:
image: nginx:alpine
ports:
- "8080:8080"
volumes:
- ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf
depends_on:
- catalog-service
- playback-service
- cdn-edge
What does this code do?
The cdn-origin service serves video files from disk using nginx. The cdn-edge service sits in front of it as a caching layer. Together they simulate Netflix's Open Connect CDN architecture.
The gateway is the single entry point that routes traffic to the right backend. It maps to Netflix's Zuul API gateway in production.
- Save docker-compose.yml.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file.
ⓧ I'd like to double check the full code
Your docker-compose.yml should contain two sections: the first chunk (redis, catalog-service, playback-service) and the second chunk (cdn-origin, cdn-edge, gateway). Scroll up and confirm each section is present in order, then save the file.
Write and run the capacity estimation script
Every Netflix system design interview starts with scale. How many users? How many requests per second? How much bandwidth? This script answers those questions with real math so you can justify why the architecture needs separate systems for API traffic and video delivery.
- Create a file called capacity_estimation.py in the root of your netflix-system-design folder, then paste the following code to set up the calculations:
"""
Netflix-Scale Capacity Estimation
Run this to understand the traffic that drives every architectural decision.
"""
def estimate():
total_subscribers = 300_000_000
peak_concurrent_ratio = 0.10
peak_concurrent_users = int(total_subscribers * peak_concurrent_ratio)
avg_session_min = 60
playback_starts_per_session = 2.5
segment_duration_sec = 4
avg_bitrate_mbps = 5
playback_starts_per_sec = int(
peak_concurrent_users * playback_starts_per_session / (avg_session_min * 60)
)
catalog_reads_per_sec = peak_concurrent_users * 3
metadata_reads_per_sec = peak_concurrent_users * 2
segments_per_sec = int(peak_concurrent_users / segment_duration_sec)
data_plane_tbps = round(peak_concurrent_users * avg_bitrate_mbps / 1_000_000, 1)
control_plane_rps = catalog_reads_per_sec + metadata_reads_per_sec + playback_starts_per_sec
What does this code do?
The script starts from one real number: 300 million subscribers. It assumes 10% are watching at peak (30 million concurrent users), then calculates how many catalog reads, metadata reads, and playback starts hit the control plane every second.
Separately, it calculates the data plane load: how many video segments per second and how many terabits of bandwidth that requires. The variable control_plane_rps sums up all API traffic. The variable data_plane_tbps captures the video bandwidth. Comparing these two numbers is the whole point.
- Now add the output section below the control_plane_rps line in the same file. This prints the control plane and data plane numbers:
print("=" * 60)
print("NETFLIX-SCALE CAPACITY ESTIMATION")
print("=" * 60)
print(f"\nTotal subscribers: {total_subscribers:>15,}")
print(f"Peak concurrent users: {peak_concurrent_users:>15,}")
print(f"Playback starts/sec: {playback_starts_per_sec:>15,}")
print()
print("-" * 60)
print("CONTROL PLANE (API servers on AWS)")
print("-" * 60)
print(f" Catalog reads/sec: {catalog_reads_per_sec:>15,}")
print(f" Metadata reads/sec: {metadata_reads_per_sec:>15,}")
print(f" Playback starts/sec: {playback_starts_per_sec:>15,}")
print(f" TOTAL control plane: {control_plane_rps:>15,} req/s")
print()
print("-" * 60)
print("DATA PLANE (Open Connect CDN)")
print("-" * 60)
print(f" Segment fetches/sec: {segments_per_sec:>15,}")
print(f" Bandwidth: {data_plane_tbps:>15} Tbps")
What does this code do?
This section formats the numbers into a readable table split into two sections: the control plane (lightweight API requests handled by AWS) and the data plane (massive video byte delivery handled by the CDN). The contrast between these two sections is what makes the case for separating them.
- Finally, add the "why this matters" summary and the main guard at the bottom of the file:
print()
print("-" * 60)
print("WHY THIS MATTERS")
print("-" * 60)
print(f" Data plane bandwidth is {int(data_plane_tbps * 1000)} Gbps.")
print(" This is why video bytes NEVER flow through the control plane.")
print(" The control plane handles lightweight API requests.")
print(" The data plane handles massive video byte delivery.")
print(" They must be completely separate systems.")
print()
print(f" At {catalog_reads_per_sec:,} catalog reads/sec with 50ms DB latency,")
print(f" you'd need ~{catalog_reads_per_sec * 50 // 1000:,} server-seconds of compute/sec.")
print(" EVCache (Redis) drops read latency to <1ms, cutting servers by 50x.")
print("=" * 60)
if __name__ == "__main__":
estimate()
What does this code do?
The "WHY THIS MATTERS" section connects the numbers to the architecture. It explains why video bytes must stay off the API servers and why the catalog needs aggressive caching (EVCache). These are the talking points you will back up with real benchmarks in later steps.
The if __name__ == "__main__" guard lets you run the script directly from the terminal.
- Save capacity_estimation.py.
Before you run the script, what do you think the ratio between control plane requests and data plane bandwidth will look like? Will one dwarf the other?
- Run the capacity estimation script by running this command:
python3 capacity_estimation.py
You should see a formatted table. The control plane total is around 150 million requests per second. The data plane bandwidth is 150 Tbps. The gap between these two numbers is enormous, and it is the reason Netflix runs them as completely separate systems.
Seeing a syntax error or no output?
Make sure you pasted all three chunks into a single capacity_estimation.py file, in order. The print statements must be indented inside the estimate() function (four spaces of indentation).
Help me fix my capacity estimation script.
✔️ Awesome, I've got everything!
Great. Double check you have saved the file.
ⓧ I'd like to double check the full code
Your capacity_estimation.py should contain three sections: the function definition with calculations (chunk 1), the control/data plane table (chunk 2), and the "WHY THIS MATTERS" block plus the main guard (chunk 3). Scroll up and confirm each section is present in order inside the estimate() function, then save the file.
Encode sample video into HLS quality levels
Netflix encodes every title at multiple bitrates so the player can switch quality based on the viewer's bandwidth. This is called adaptive bitrate streaming. You will generate a short test video and encode it at three quality levels to populate the CDN.
- Create the output directories for each quality level by running this command:
mkdir -p cdn/origin/videos/sample/360p cdn/origin/videos/sample/480p cdn/origin/videos/sample/720p
- Encode the 360p quality level (400 kbps) by running this command:
ffmpeg -f lavfi -i testsrc=duration=10:size=640x360:rate=24 \
-c:v libx264 -b:v 400k -pix_fmt yuv420p \
-f hls -hls_time 4 -hls_list_size 0 -hls_playlist_type vod \
cdn/origin/videos/sample/360p/playlist.m3u8
What does this command do?
The -f lavfi -i testsrc flags tell FFmpeg to generate a 10-second test pattern instead of reading a real video file. The -c:v libx264 flag encodes it with H.264 at 400 kbps. The -f hls flags output it as HLS segments (.ts files) with a playlist.m3u8 index.
- Confirm the 360p segments were created by running:
ls cdn/origin/videos/sample/360p/
You should see playlist.m3u8 and several .ts segment files.
- Encode the 480p quality level (800 kbps) by running this command:
ffmpeg -f lavfi -i testsrc=duration=10:size=854x480:rate=24 \
-c:v libx264 -b:v 800k -pix_fmt yuv420p \
-f hls -hls_time 4 -hls_list_size 0 -hls_playlist_type vod \
cdn/origin/videos/sample/480p/playlist.m3u8
- Encode the 720p quality level (1500 kbps) by running this command:
ffmpeg -f lavfi -i testsrc=duration=10:size=1280x720:rate=24 \
-c:v libx264 -b:v 1500k -pix_fmt yuv420p \
-f hls -hls_time 4 -hls_list_size 0 -hls_playlist_type vod \
cdn/origin/videos/sample/720p/playlist.m3u8
- Confirm all three quality levels are encoded by running:
ls cdn/origin/videos/sample/
You should see 360p, 480p, and 720p directories listed.
FFmpeg command failed?
If you see an error about libx264 not being found, your FFmpeg installation may be missing the H.264 encoder. Reinstall with Homebrew using brew reinstall ffmpeg which includes all common encoders.
Help me fix my FFmpeg encoding command.
Now you need a master playlist that tells video players which quality levels are available. Netflix's client reads this manifest to decide which bitrate to request based on the viewer's bandwidth.
- Create the master.m3u8 file in cdn/origin/videos/sample/ and paste the following content:
#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=400000,RESOLUTION=640x360
360p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=800000,RESOLUTION=854x480
480p/playlist.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=1500000,RESOLUTION=1280x720
720p/playlist.m3u8
What does this file do?
The master.m3u8 is an HLS master playlist. Each #EXT-X-STREAM-INF line declares a quality variant with its bandwidth and resolution. The player reads this manifest, picks the best quality for the viewer's connection, and fetches segments from the matching sub-playlist.
- Save master.m3u8.
- Verify the manifest contents by running:
cat cdn/origin/videos/sample/master.m3u8
You should see three #EXT-X-STREAM-INF entries for 360p, 480p, and 720p, each pointing to its playlist.m3u8 file.
Your foundation is set. You have the numbers that justify every architectural decision, a full Docker Compose blueprint, and sample video content ready for the CDN. Next up, you will build the catalog service and see why those 90 million catalog reads per second need aggressive caching.
Build the Catalog Service
Your capacity estimation quantified the scale driving Netflix's architecture. Now it is time to build the first component that handles that read traffic.
The catalog service is Netflix's most heavily read control plane service. Every browse, search, and home screen render hits it. You will build it with FastAPI and add simulated database latency so you can measure the raw cost of every request.
In this step, get ready to:
- Write the catalog service with FastAPI and simulated 50ms database latency.
- Package it with a Dockerfile and bring it up with Docker Compose.
- Confirm every request takes ~50ms with no caching.
Set up the service packaging
Before writing the application code, you need the dependency list and container definition that Docker uses to build the service image.
- Create a file called requirements.txt inside the catalog-service folder with the following contents:
fastapi==0.141.1
uvicorn==0.52.4
redis==8.1.0
What are these packages?
- fastapi is the web framework for building the API endpoints.
- uvicorn is the ASGI server that runs the FastAPI app.
- redis is the Python client for connecting to Redis. You will use it in a later step.
- Create a file called Dockerfile inside the catalog-service folder with this content:
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
# Install dependencies first for Docker layer caching
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
# Start the FastAPI server on port 8001
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
What does this Dockerfile do?
It starts from a slim Python 3.12 image, installs your dependencies from requirements.txt, copies your application code, and starts uvicorn on port 8001.
Copying requirements.txt and running pip install before copying main.py is intentional. Docker caches each layer, so dependencies only reinstall when requirements.txt changes.
- Confirm both files exist by running:
ls catalog-service/
You should see Dockerfile and requirements.txt listed in the output.
Write the catalog API
The catalog service is a FastAPI application that returns title metadata. Each request simulates a 50ms database round-trip so you can measure the raw latency later.
- Create a file called main.py inside the catalog-service folder. Start with the imports, app setup, and the first catalog entry:
import asyncio
import json
import os
import time
import redis
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
# Create the FastAPI app with CORS for cross-origin access
app = FastAPI()
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
# Connect to Redis (used for caching in a later step)
redis_client = redis.Redis(
host=os.getenv("REDIS_HOST", "localhost"), port=6379, decode_responses=True
)
# Simulated database of Netflix titles
CATALOG = [
{
"id": "sample",
"title": "Big Buck Bunny",
"description": "A large rabbit deals with three bullying rodents.",
"genres": ["animation", "comedy", "short"],
"tags": ["animals", "outdoors", "slapstick", "family"],
},
What does this setup do?
- The FastAPI() app and CORS middleware let any client (browser, curl) access the API.
- redis_client initializes a Redis connection. You are not using it for caching yet, but it will be ready when you need it.
- CATALOG is a Python list acting as your simulated database. Each entry has an id, title, description, genres, and tags.
- Continue adding the next four catalog entries directly below the first entry in main.py:
{
"id": "elephant",
"title": "Elephants Dream",
"description": "Two characters explore a surreal mechanical world.",
"genres": ["animation", "sci-fi", "short"],
"tags": ["surreal", "machines", "exploration", "abstract"],
},
{
"id": "sintel",
"title": "Sintel",
"description": "A lone warrior searches for a baby dragon.",
"genres": ["animation", "fantasy", "adventure"],
"tags": ["dragons", "quest", "warrior", "epic"],
},
{
"id": "tears",
"title": "Tears of Steel",
"description": "A group of warriors and scientists fight robots in Amsterdam.",
"genres": ["sci-fi", "action", "short"],
"tags": ["robots", "future", "combat", "urban"],
},
{
"id": "cosmos",
"title": "Cosmos Laundromat",
"description": "A sheep on a remote island discovers parallel lives.",
"genres": ["animation", "comedy", "drama"],
"tags": ["sheep", "philosophy", "parallel-worlds", "island"],
},
- Save main.py and confirm it appears in the folder by running:
ls catalog-service/
You should now see Dockerfile, main.py, and requirements.txt listed.
- Reopen main.py and add the last catalog entry, close the list, and add both API endpoints at the bottom:
{
"id": "spring",
"title": "Spring",
"description": "A shepherd girl and her dog discover a strange creature in the forest.",
"genres": ["animation", "fantasy", "short"],
"tags": ["nature", "forest", "creatures", "pastoral"],
},
]
@app.get("/api/catalog")
async def get_catalog():
start = time.perf_counter()
# Simulate a 50ms Cassandra round-trip
await asyncio.sleep(0.05)
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"source": "database", "latency_ms": elapsed_ms, "titles": CATALOG}
@app.get("/api/catalog/{title_id}")
async def get_title(title_id: str):
start = time.perf_counter()
# Simulate a 50ms Cassandra round-trip
await asyncio.sleep(0.05)
for title in CATALOG:
if title["id"] == title_id:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"source": "database", "latency_ms": elapsed_ms, "title": title}
return {"error": "not found"}
What do these endpoints do?
- asyncio.sleep(0.05) pauses for 50 milliseconds on every request. This simulates the round-trip time to a real Cassandra database that Netflix uses for catalog metadata.
- time.perf_counter() measures the actual elapsed time and includes it in the response as latency_ms. This lets you see exactly how long each request takes.
- /api/catalog returns all titles. /api/catalog/{title_id} returns a single title by ID.
- Every response includes "source": "database" because every request hits the simulated database. There is no shortcut yet.
- Save main.py.
✔️ Awesome, I've got everything!
Great. Double check that you have saved main.py, requirements.txt, and Dockerfile inside the catalog-service folder.
ⓧ I'd like to double check the full code
Scroll up and confirm each section is present in main.py, in this order:
- Imports: asyncio, json, os, time, redis, FastAPI, CORSMiddleware.
- App setup with CORS middleware and redis_client initialization.
- CATALOG list with all 6 entries: sample, elephant, sintel, tears, cosmos, spring.
- get_catalog endpoint at /api/catalog with asyncio.sleep(0.05).
- get_title endpoint at /api/catalog/{title_id} with asyncio.sleep(0.05).
Also confirm requirements.txt lists fastapi==0.141.1, uvicorn==0.52.4, and redis==8.1.0.
Confirm Dockerfile starts from python:3.12-slim and runs uvicorn on port 8001.
Bring up the service and confirm the latency
The docker-compose.yml you created in Step 1 already defines the catalog-service and redis services. Now that the source files exist, Docker Compose can build the image and start both containers.
- Build and start the catalog service and Redis by running:
docker compose up --build catalog-service redis
What does this command do?
Docker Compose builds the catalog-service image from your Dockerfile, pulls the redis:7-alpine image, and starts both containers. The --build flag forces a fresh image build so any code changes are picked up.
This command runs in the foreground and shows container logs. When you see a line from uvicorn indicating the server is running on port 8001, the service is ready.
Build failing?
- Make sure Docker Desktop is running. If you see a connection error, open Docker Desktop and wait for it to finish starting.
- Check that Dockerfile, requirements.txt, and main.py are all inside the catalog-service folder, not the project root.
- If the build hangs on installing dependencies, it may be a slow network connection. Wait a few minutes for the packages to download.
Help me debug my Docker build error
- Open a new terminal tab or window. Keep the Docker Compose terminal running so the services stay up.
- In the new terminal, navigate back to the folder containing your docker-compose.yml file.
Before you hit the catalog endpoint, what do you think the response will include? What value do you expect for latency_ms?
- Test the catalog endpoint by running:
curl http://localhost:8001/api/catalog
You should see a JSON response containing "source": "database" and a latency_ms value around 50ms, followed by the full titles list.
- Hit the same endpoint two more times by running the same curl command again:
curl http://localhost:8001/api/catalog
Every single response shows "source": "database" and a latency_ms hovering around 50ms. No matter how many times you repeat the request, the latency stays the same.
Why is every request exactly the same speed?
There is no cache sitting between you and the simulated database. Every request pays the full 50ms round-trip cost.
Your capacity estimation showed the catalog handles massive read traffic at peak. Serving all of those reads at 50ms each would require an enormous number of backend instances.
- Test a single-title lookup by running:
curl http://localhost:8001/api/catalog/sample
You should see a response with "source": "database" and a single title object for Big Buck Bunny, also taking ~50ms.
Not seeing the expected response?
- If curl shows a connection error, make sure the Docker Compose terminal still shows the service running. If it crashed, check the logs for a Python traceback.
- If you see an empty response or a 404, confirm your main.py has both the @app.get("/api/catalog") and @app.get("/api/catalog/{title_id}") decorators.
Help me troubleshoot my catalog service
Your catalog service is running and returning data in ~50ms on every request. The next step digs into whether that is fast enough at Netflix scale.
Benchmark, Then Add the EVCache Layer
Your catalog service is live. Every request returns in about 50ms, which feels perfectly responsive when you test it with curl.
Your capacity estimation told a different story. At Netflix scale, the catalog service gets hammered with millions of reads per second. At 50ms per read, you would need a massive fleet of backend instances just to keep up. Netflix solved this with EVCache, a Memcached fork that serves catalog data from memory. You'll add Redis as the EVCache stand-in and prove the improvement with real numbers.
In this step, get ready to:
- Write a benchmark script and record baseline catalog latency.
- Add Redis caching (the EVCache pattern) to the catalog service.
- Re-benchmark and measure the improvement with real numbers.
Write the benchmark script and record baseline latency
Before adding any caching, you need hard numbers that prove the problem. A benchmark script will send 100 requests to the catalog service and report percentile latencies.
- Create a new file called benchmark.py in the root of your netflix-system-design folder (the same level as docker-compose.yml).
- Add the benchmark function's core measurement logic by pasting this code:
"""
Benchmark the catalog service latency.
Run before and after adding Redis caching to see the difference.
"""
import time
import urllib.request
def benchmark(url, num_requests=100):
# Send requests and record how long each takes
latencies = []
for i in range(num_requests):
start = time.perf_counter()
urllib.request.urlopen(url).read()
elapsed_ms = (time.perf_counter() - start) * 1000
latencies.append(elapsed_ms)
# Sort latencies to compute percentiles
latencies.sort()
n = len(latencies)
p50 = latencies[n // 2]
p99 = latencies[int(n * 0.99)]
avg = sum(latencies) / n
# Print percentile breakdown
print(f"\nBenchmark: {num_requests} requests to {url}")
print(f" p50: {p50:>8.1f} ms")
print(f" p99: {p99:>8.1f} ms")
print(f" avg: {avg:>8.1f} ms")
print(f" min: {min(latencies):>8.1f} ms")
print(f" max: {max(latencies):>8.1f} ms")
What does this code do?
The benchmark function fires 100 HTTP requests at a URL and records how long each one takes using time.perf_counter().
After collecting all 100 latencies, it sorts them to compute percentiles. p50 is the median (half of requests are faster). p99 is the worst-case for 99% of requests. These are the same metrics Netflix engineers use to evaluate service health.
- Now add the source detection and the entry point below the last print line, still inside benchmark.py:
# Check whether responses come from cache or database
first_source = "unknown"
try:
import json
resp = urllib.request.urlopen(url).read()
data = json.loads(resp)
first_source = data.get("source", "unknown")
except Exception:
pass
print(f" source: {first_source}")
if __name__ == "__main__":
benchmark("http://localhost:8001/api/catalog")
What does the source detection do?
After the benchmark finishes, the script sends one more request and reads the source field from the JSON response. This tells you whether the catalog service is serving from "database" or "cache". Right now, it will always say database.
- Save benchmark.py.
✔️ Awesome, I've got everything!
Great. Double check you've saved the file before running it.
ⓧ I'd like to double check the full code
Scroll up and confirm both chunks are present in benchmark.py, in this order:
- The docstring, imports, and the benchmark function with the timing loop, percentile calculation, and print statements.
- The source detection block (first_source / try block) still indented inside the function, followed by the if __name__ == "__main__": block at the top level.
Before you run the benchmark, what latency do you expect to see? Every catalog request includes a 50ms simulated database delay. Will the average be exactly 50ms, or slightly higher?
- Make sure your catalog service and Redis are still running from the previous step. If you stopped them, restart by running:
docker compose up --build catalog-service redis
- Open a separate terminal window in your project directory and run the baseline benchmark:
python3 benchmark.py
You should see output showing p50 around 55ms, p99 around 65ms, and the source as database. The extra ~5ms on top of the 50ms sleep is network overhead from the Docker container.
Why these numbers matter
At ~55ms per request, every single catalog read costs a database round-trip. Your capacity estimation showed the catalog gets read millions of times per second at peak. Multiplying 55ms by that volume means you would need thousands of backend instances just to absorb the load.
Seeing a connection error?
- Make sure Docker containers are running. Run docker compose ps and confirm both catalog-service and redis show as running.
- Confirm you are running python3 benchmark.py from the project root directory, not from inside a service folder.
Help me debug my benchmark script connection error
Add Redis caching to the catalog service
Netflix uses EVCache to serve catalog reads from memory instead of hitting the database every time. Catalog data changes slowly, so a short TTL (time to live) means nearly all reads are cache hits. You'll replicate this pattern with Redis.
- Open catalog-service/main.py.
- Find the get_catalog function (the one decorated with @app.get("/api/catalog")).
- Replace the entire function with this version that checks Redis before hitting the database:
@app.get("/api/catalog")
async def get_catalog():
start = time.perf_counter()
# Check Redis cache first (EVCache pattern)
cached = redis_client.get("catalog")
if cached:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"source": "cache", "latency_ms": elapsed_ms, "titles": json.loads(cached)}
# Cache miss: simulate database query (50ms round-trip)
await asyncio.sleep(0.05)
# Store result in Redis with 60-second TTL
redis_client.setex("catalog", 60, json.dumps(CATALOG))
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"source": "database", "latency_ms": elapsed_ms, "titles": CATALOG}
What does this code do?
On every request, the function first calls redis_client.get("catalog"). If the key exists (cache hit), it skips the 50ms database delay entirely and returns instantly from memory.
On a cache miss, it simulates the database query, then stores the result in Redis using setex with a 60-second TTL. Every subsequent request within that 60-second window is a cache hit. The source field in the response tells you which path each request took.
Why a 60-second TTL?
Netflix's catalog (titles, descriptions, genres) changes slowly. New releases happen a few times per week, not per second. A short TTL like 60 seconds ensures the cache stays fresh enough while absorbing the vast majority of read traffic.
- Now find the get_title function (decorated with @app.get("/api/catalog/{title_id}")).
- Replace it with this version that caches individual title lookups:
@app.get("/api/catalog/{title_id}")
async def get_title(title_id: str):
start = time.perf_counter()
# Check Redis cache for this specific title
cached = redis_client.get(f"title:{title_id}")
if cached:
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"source": "cache", "latency_ms": elapsed_ms, "title": json.loads(cached)}
# Cache miss: simulate database query
await asyncio.sleep(0.05)
for title in CATALOG:
if title["id"] == title_id:
# Cache individual title with 60-second TTL
redis_client.setex(f"title:{title_id}", 60, json.dumps(title))
elapsed_ms = round((time.perf_counter() - start) * 1000, 2)
return {"source": "database", "latency_ms": elapsed_ms, "title": title}
return {"error": "not found"}
What does this code do?
This follows the same cache-first pattern, but uses a per-title key like title:sample or title:sintel. Each title gets its own cache entry with its own 60-second TTL, so looking up one title doesn't force a cache refresh of the entire catalog.
- Save catalog-service/main.py.
✔️ Awesome, I've got everything!
Great. Make sure both functions are saved before rebuilding the service.
ⓧ I'd like to double check the full code
Your catalog-service/main.py file is long, so scroll through and confirm these sections are present in order:
- Imports at the top: asyncio, json, os, time, redis, and FastAPI.
- The redis_client connection using REDIS_HOST environment variable.
- The CATALOG list with 6 titles (sample, elephant, sintel, tears, cosmos, spring).
- The updated get_catalog function with Redis cache check, setex on miss, and source field in the response.
- The updated get_title function with per-title cache keys like title:{title_id}.
Seeing a syntax error after saving?
- Make sure you replaced the entire old function, including the @app.get decorator line. Leaving the old decorator above the new one causes a duplicate route error.
- Check that the json import is present at the top of the file. The new code uses json.loads() and json.dumps().
Help me fix my catalog service after adding Redis caching
Re-benchmark and see the difference
The caching code is saved, but the Docker container is still running the old version. You need to rebuild the image so the container picks up your changes.
- In the terminal where Docker Compose is running, press Ctrl+C to stop the services.
- Rebuild and restart the catalog service and Redis by running:
docker compose up --build catalog-service redis
- Wait until you see the uvicorn startup log line confirming the service is ready.
- Verify the caching works by hitting the endpoint with curl in a separate terminal:
curl http://localhost:8001/api/catalog
The first response should show "source": "database" with a latency around 55ms. That's the cache miss.
- Run the same curl command again:
curl http://localhost:8001/api/catalog
This time, the response should show "source": "cache" with a latency under 5ms. The 50ms database delay is completely gone.
Now, before you run the full benchmark, think about what the numbers will look like. The first request out of 100 will be a cache miss. What about the other 99?
- Run the benchmark again to measure the improvement:
python3 benchmark.py
The results should show p50 under 5ms and p99 under 10ms, with the source reported as cache. Compare that to your baseline of ~55ms p50. That is a 10x improvement or more.
Why Netflix uses EVCache
Your benchmark just proved the pattern Netflix relies on at scale. Catalog data changes slowly (new titles appear a few times per week), so a 60-second TTL means nearly every read is a cache hit. The first user after a cache expiry pays the 50ms database cost. Everyone else gets sub-millisecond reads from memory.
At Netflix's volume, this caching layer is what makes the catalog service viable. Without it, the database would need to absorb every single read, requiring a dramatically larger backend fleet.
Still seeing ~55ms on every request?
- Make sure you rebuilt the Docker image. Running docker compose up without the --build flag reuses the old image that does not have your caching code.
- Verify Redis is running by checking docker compose ps. If Redis is not listed, the cache calls silently fail and the service falls through to the database path every time.
- Check that you replaced the old get_catalog function entirely. If any part of the old function remains, the cache logic may not be running.
Help me debug why my Redis caching is not improving latency
You just proved with real numbers why Netflix caches catalog reads in EVCache. Next up, you'll build the playback service and make the control plane and data plane split visible. Video bytes will flow through a completely separate path from the API calls you just benchmarked.
Build the Playback Service and Separate the Planes
Your benchmark proved that Redis caching slashes catalog read latency from ~55ms to under 5ms. That solves the speed problem for the control plane.
Your capacity estimation also showed a data plane carrying over 150 Tbps of video traffic at peak. In this step, you'll build the playback service and wire everything through an API gateway that makes the control/data plane split visible.
In this step, get ready to:
- Build the playback service that returns manifest URLs and CDN endpoints.
- Configure CDN origin and edge servers to handle video content delivery.
- Set up an nginx gateway that routes control plane and data plane traffic to separate backends.
Write the playback service
When a user presses play, Netflix's Playback API returns a manifest URL and a list of CDN endpoints. The video bytes never flow through the playback service itself.
- Create playback-service/requirements.txt with the service dependencies by pasting this code:
- Create playback-service/Dockerfile with the container configuration by pasting this code:
fastapi==0.141.1
uvicorn==0.52.4
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY main.py .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]
What do these files set up?
The requirements file pins FastAPI and uvicorn at the same versions you used for the catalog service. The Dockerfile follows the same pattern too: install dependencies, copy the app, and run uvicorn on port 8002.
- Write the playback application code by pasting this into playback-service/main.py:
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
app = FastAPI()
app.add_middleware(
CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
)
# Available quality levels matching the HLS encodes from Step 1
QUALITY_LEVELS = [
{"resolution": "640x360", "bitrate": 400000, "label": "360p"},
{"resolution": "854x480", "bitrate": 800000, "label": "480p"},
{"resolution": "1280x720", "bitrate": 1500000, "label": "720p"},
]
@app.get("/api/playback/{title_id}")
def get_playback_manifest(title_id: str):
# Return manifest URL and CDN info - never proxy actual video bytes
return {
"title_id": title_id,
"manifest_url": f"/videos/{title_id}/master.m3u8",
"cdn_base_url": "/videos",
"quality_levels": QUALITY_LEVELS,
"drm": "none",
}
What does this code do?
The QUALITY_LEVELS list mirrors the three HLS quality levels you encoded in Step 1 (360p, 480p, 720p). Each entry records the resolution and bitrate so a client knows what's available.
The /api/playback/{title_id} endpoint returns a manifest_url pointing to the CDN path and a cdn_base_url telling the client where video segments live. It never touches video bytes.
- Save all three files.
- Start the playback service in detached mode by running this command:
docker compose up --build playback-service -d
- Test the endpoint by running this command:
curl http://localhost:8002/api/playback/sample
You should see JSON containing manifest_url set to /videos/sample/master.m3u8, cdn_base_url set to /videos, and all three quality levels listed. No video data anywhere in the response.
Seeing a connection refused error?
- Run docker compose ps to check if the playback-service container is running. If it exited, check the logs with docker compose logs playback-service.
- Make sure your Dockerfile specifies port 8002 in the CMD line.
- Confirm nothing else is using port 8002 on your machine.
Help me debug my playback service not starting in Docker.
Configure the CDN and API gateway
Your playback service returns URLs pointing to /videos/. Now you need something at that path to actually serve the video files. You also need a single entry point that routes API requests and video requests to different backends.
- Configure the CDN origin server by pasting this into cdn/origin/nginx.conf:
server {
listen 8003;
location /videos/ {
alias /usr/share/nginx/html/videos/;
add_header X-Served-By "cdn-origin";
add_header Access-Control-Allow-Origin *;
# Map HLS file extensions to correct MIME types
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
}
}
}
- Configure the CDN edge server by pasting this into cdn/edge/nginx.conf:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=edge_cache:10m max_size=100m inactive=60m use_temp_path=off;
server {
listen 8004;
location /videos/ {
proxy_cache edge_cache;
proxy_cache_valid 200 60m;
# Fetch from origin on cache miss
proxy_pass http://cdn-origin:8003;
proxy_set_header Host $host;
# Expose cache status in response headers
add_header X-Cache-Status $upstream_cache_status;
add_header X-Served-By "cdn-edge";
add_header Access-Control-Allow-Origin *;
}
}
What do these two configs do?
The origin server (cdn-origin) serves video files directly from disk on port 8003. It adds an X-Served-By: cdn-origin header so you can trace where content came from.
The edge server (cdn-edge) sits in front of the origin on port 8004. It uses nginx proxy_cache to cache responses from origin. The X-Cache-Status header will show whether each request was a cache MISS or HIT. You'll explore this behavior closely in Step 5.
- Save both files.
- Start the CDN origin service to verify it can serve your video files by running this command:
docker compose up --build cdn-origin -d
- Fetch the HLS manifest directly from the origin by running this command:
curl http://localhost:8003/videos/sample/master.m3u8
You should see the HLS master playlist you created in Step 1, with the three #EXT-X-STREAM-INF lines listing each quality level.
Getting an empty response or 404?
- Check that cdn/origin/videos/sample/master.m3u8 exists on disk. This file was created in Step 1.
- Confirm your cdn/origin/nginx.conf uses alias (not root). The alias directive replaces the matched path prefix with the specified directory.
Help me debug my nginx CDN origin not serving video files.
Now you need a single gateway that routes all traffic. Nginx will act as the API gateway, standing in for Netflix's Zuul.
- Create gateway/nginx.conf and start by defining the upstream service pools:
upstream catalog_service {
server catalog-service:8001;
}
upstream playback_service {
server playback-service:8002;
}
upstream cdn_edge {
server cdn-edge:8004;
}
- Add the server block below the upstream definitions in the same file:
server {
listen 8080;
# Control plane: catalog browsing
location /api/catalog {
proxy_pass http://catalog_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Control plane: playback manifest requests
location /api/playback {
proxy_pass http://playback_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Data plane: video content via CDN edge
location /videos/ {
proxy_pass http://cdn_edge;
proxy_set_header Host $host;
}
# Default health check endpoint
location / {
return 200 '{"service": "netflix-gateway", "status": "ok"}';
add_header Content-Type application/json;
}
}
What does the gateway do?
The gateway listens on a single port (8080) and splits traffic by URL path. Requests starting with /api/ go to the FastAPI services. Requests starting with /videos/ go to the CDN edge.
This is the control plane vs data plane split in action. Lightweight API requests (catalog browsing, playback starts) flow through one set of backends. Heavy video byte delivery flows through a completely separate set.
- Save gateway/nginx.conf.
✔️ Awesome, I've got everything!
Great. Double check you've saved gateway/nginx.conf.
ⓧ I'd like to double check the full code
Scroll up and confirm your gateway/nginx.conf contains both blocks in order:
- Three upstream definitions (catalog_service, playback_service, cdn_edge).
- One server block on port 8080 with four location blocks: /api/catalog, /api/playback, /videos/, and a default / route.
Bring everything up and verify the split
Every config file is in place. Time to bring up the full system and prove that the control plane and data plane are separated.
- Stop any running containers from earlier tests by running this command:
docker compose down
- Start all services together by running this command:
docker compose up --build
What should I see?
Docker Compose builds and starts all six services: redis, catalog-service, playback-service, cdn-origin, cdn-edge, and gateway. This command takes over your terminal, so you'll need a second terminal for the next steps.
Press Cmd+T (macOS) or Ctrl+Shift+T (Windows) to open a new terminal tab.
Before you run the next command, think about this: when you hit the playback endpoint, will the response contain video data or something else?
- In your new terminal, hit the playback endpoint through the gateway by running this command:
curl http://localhost:8080/api/playback/sample
You see JSON with a manifest_url pointing to /videos/sample/master.m3u8 and a cdn_base_url of /videos. No video bytes. The playback service tells the client where to find the video, then gets out of the way.
Now consider: if you request an actual video file path through the same gateway port, will it reach the playback service or a different backend entirely?
- Fetch the video manifest through the CDN path by running this command:
curl http://localhost:8080/videos/sample/master.m3u8
You see the raw HLS manifest content with #EXTM3U and the three #EXT-X-STREAM-INF entries. This came from the CDN, not from the playback service.
Why does this split matter?
Both requests entered through the same gateway on port 8080. The /api/playback path routed to a lightweight FastAPI service that returned a few hundred bytes of JSON. The /videos/ path routed to the CDN, which can serve terabytes of video content.
In production, Netflix's API servers on AWS never see a single video byte. All video traffic flows through Open Connect, their CDN network inside ISP data centers. Without this separation, the API servers would need to handle over 150 Tbps of bandwidth.
Gateway returning 502 Bad Gateway?
- Run docker compose ps in another terminal and check that all six services show a healthy status.
- If cdn-edge exited, check docker compose logs cdn-edge. A common cause is a typo in cdn/edge/nginx.conf preventing nginx from starting.
- If gateway exited, check docker compose logs gateway. Verify that your upstream names match the service names in docker-compose.yml exactly.
Help me debug my nginx gateway returning 502 errors in Docker Compose.
Your control plane and data plane are now running on separate backends, all visible through the gateway's routing rules. Next up, you'll put the CDN edge cache to the test and measure how cache hits eliminate origin fetches.
Simulate Open Connect CDN with Edge Caching
In the previous step, you built the playback service and proved the control/data plane split. Video requests take a completely separate path through the CDN, never touching your API servers.
Netflix's Open Connect CDN places cache appliances inside ISP networks worldwide. When you press play, the nearest edge server checks its local cache. On a hit, it serves the video instantly. On a miss, it fetches from origin, caches the response, and serves all future requests locally.
Your two-tier CDN is already running with an origin server and an edge cache layer. In this step, you'll observe this MISS-to-HIT lifecycle through response headers and measure your own cache hit ratio from a batch of requests.
In this step, get ready to:
- Observe cache MISS and HIT behavior through X-Cache-Status response headers.
- Measure your local cache hit ratio from a batch of curl requests.
Clear the edge cache and observe a cache MISS
You've already been fetching video content through the CDN in the previous step. The edge cache might still have entries stored from those requests. You need a clean slate to watch the full lifecycle from the beginning.
- Clear the edge cache by restarting the cdn-edge container. Run this in your terminal:
docker compose restart cdn-edge
- You should see a message confirming cdn-edge has restarted. Wait about 3 seconds for it to come back up.
Why does restarting clear the cache?
The edge cache lives in /var/cache/nginx inside the container's filesystem. When the container restarts, that filesystem resets and all cached responses are gone. This gives you a cold cache to observe the very first request.
Before you run the next command, think about this: the edge cache is now empty. What cache status do you expect to see on the very first request?
- Fetch the manifest response headers through the gateway by running:
curl -I http://localhost:8080/videos/sample/master.m3u8
Look for two headers in the response. X-Cache-Status: MISS tells you the edge didn't have this content cached, so it had to fetch from origin. X-Served-By: cdn-edge confirms the response came through the edge layer.
What just happened behind the scenes?
Your request entered the gateway on port 8080, which routed it to the edge on port 8004. The edge checked its proxy_cache (empty after the restart) and found nothing.
So the edge forwarded the request to cdn-origin:8003, received the manifest file, stored it in the local cache, and returned it to you. This is exactly what happens at a Netflix Open Connect Appliance when a subscriber is the first person at that ISP to request a particular segment.
Not seeing X-Cache-Status in the headers?
- Make sure you waited a few seconds after the restart. The edge container needs time to come back up. Try running the curl command again.
- If you see a connection refused error, the container may still be starting. Wait 5 more seconds and retry.
- Check that all services are running by running docker compose ps. Every service should show a running state.
Why am I not seeing cache status headers from the CDN edge?
Observe a cache HIT on the second request
The edge just fetched the manifest from origin and cached it locally. What do you expect the cache status to be if you send the exact same request?
- Run the same curl command again:
curl -I http://localhost:8080/videos/sample/master.m3u8
This time you should see X-Cache-Status: HIT and X-Served-By: cdn-edge. The edge served the manifest directly from its local cache without contacting the origin server at all.
The MISS-then-HIT lifecycle
This is the fundamental pattern of every CDN. The first request is a "cold" cache miss that warms the cache. Every subsequent request within the cache window is a hit.
You configured a 60-minute cache window with proxy_cache_valid 200 60m in your edge config. Any request for this manifest within the next 60 minutes hits the cache. After that, the edge fetches a fresh copy from origin. Netflix uses the same TTL-based approach but drives even higher hit rates with proactive filling.
Measure your cache hit ratio with a batch test
A single pair of requests is convincing, but real CDN performance is measured as a hit ratio across many requests. You'll run a batch of 10 requests and calculate the percentage that were served from cache.
- Clear the edge cache again so you start with a fresh MISS by running:
docker compose restart cdn-edge
- Wait about 3 seconds for the container to come back up.
Before you run the batch, predict: out of 10 sequential requests to the same manifest, how many do you expect to be MISS and how many HIT?
- Send 10 requests and extract the cache status from each by running:
for i in $(seq 1 10); do curl -s -I http://localhost:8080/videos/sample/master.m3u8 | grep X-Cache-Status; done
What does this command do?
- for i in $(seq 1 10) runs the loop 10 times.
- curl -s -I sends a silent HEAD request and shows only response headers.
- grep X-Cache-Status filters the output to show only the cache status header from each response.
You should see 10 lines of output. The first line shows X-Cache-Status: MISS. The remaining 9 lines show X-Cache-Status: HIT. That's a 90% cache hit ratio (9 out of 10 requests served directly from the edge cache).
How does Netflix achieve even higher hit rates?
Your 90% hit ratio comes from reactive caching. The first request triggers a miss, warms the cache, and all subsequent requests are hits. Netflix goes further with proactive filling.
Netflix's fill pipeline pre-positions popular content on Open Connect Appliances overnight, before anyone requests it. A trending show gets pushed to edge caches worldwide so that even the very first subscriber request is a cache hit. This drives production hit rates to around 95-98%, keeping terabits of traffic off the origin servers entirely.
Seeing all MISS or unexpected results?
- If every request shows MISS, the cache may not be storing responses. Check that cdn/edge/nginx.conf includes the proxy_cache edge_cache directive inside the /videos/ location block.
- If you see connection errors, the cdn-edge container may not have finished restarting. Wait a few more seconds and re-run the loop.
- If you see no output at all, make sure the header name in your grep matches exactly: X-Cache-Status (case-sensitive).
Why is my CDN edge cache not storing responses between requests?
Your full Netflix-like architecture is now running end to end. You started with capacity estimation to understand the scale. You built a cached catalog service and proved with benchmarks why EVCache matters. You separated the control plane from the data plane. And you just measured your own CDN cache hit ratio. Every architectural decision is backed by numbers you collected yourself.
Secret mission
Build a Two-Stage Recommendation Engine
Netflix's recommendation system is the most-asked system design interview question after the streaming pipeline itself. In this mission, you build a two-stage recommendation engine that narrows the catalog using genre overlap (candidate generation) and then ranks results using Jaccard similarity scoring. You will wire the new service into your existing gateway and observe how different viewing histories produce different ranked results.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally in Docker containers. There are no cloud resources or ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker containers running your Netflix services (Redis, catalog-service, playback-service, recommendation-service, cdn-origin, cdn-edge, gateway).
- Docker volumes created by Compose.
- Project files on your Desktop.
- FFmpeg (installed via Homebrew).
Keep everything running
No action needed. Choose this if you're still actively building or want to demo the system to someone.
- Leave Docker Compose running to revisit your services at any time.
- Your API gateway remains accessible at http://localhost:8080.
- FFmpeg stays installed on your machine for future video encoding work.
Pause - I'll come back to this later
Stop your containers to free up memory and CPU without removing them. You can restart everything later.
- Open the terminal where Docker Compose is running and press Ctrl+C to stop the services.
- Alternatively, stop the containers by running:
docker compose stop
- When you're ready to come back, navigate to your project folder and run:
docker compose start
Delete - I don't want to use this again
Remove all containers, networks, volumes, and project files to start completely fresh.
- Stop and remove all Docker containers, networks, and volumes by running this from your project folder:
docker compose down -v
- Delete the project folder from your Desktop. On macOS or Linux, run:
cd ~/Desktop
rm -rf netflix-system-design
- On Windows, run:
cd ~\Desktop
rmdir /s /q netflix-system-design
- Uninstall FFmpeg by running:
brew uninstall ffmpeg
That's a Wrap!
That's a Wrap!
Nice work! You just built a system-design-interview-ready Netflix backend from first principles, with every architectural decision backed by numbers you collected yourself.
You've learned how to:
- Model Netflix-scale capacity and prove with benchmark data why the catalog service needs EVCache (Redis caching), showing a 10x latency improvement from ~55ms down to under 5ms.
- Build a playback service and API gateway that make the control plane / data plane split visible, where video bytes flow through the CDN and never touch the API servers.
- Simulate Netflix's Open Connect CDN with a two-tier origin + edge cache using nginx proxy_cache, observe cache MISS vs HIT behavior through response headers, and measure your own cache hit ratio.
- Secret Mission: Build a two-stage recommendation engine with candidate generation (genre filtering) and ranking (Jaccard similarity), mirroring Netflix's ML pipeline architecture.
Ready to quiz yourself?