Module 00 · Pilot Episode
Prerequisites & Environment Setup
0.1
How to Use This Course
"Scalability" is one of the most over-used and under-explained words in software. Job descriptions demand it, architecture diagrams promise it, and post-mortems blame its absence. Yet very few resources start from the beginning and explain why a system that works perfectly for 100 people collapses at 100,000 — and what, mechanically, engineers do about it.
This masterclass builds that understanding from the ground up. We start with how a single web request travels across the internet, attach real numbers to every step, and then progressively add the machinery that real production systems use: load balancers, caches, CDNs, replicated and sharded databases, message queues, containers, autoscalers and observability stacks. Every concept is introduced with a plain-language analogy first, then taken apart until the technical nuance is visible.
The format
- Modules are "episodes". Each is self-contained but builds on the previous ones. Module 00 (this one) and Module 01 are the foundation everything else rests on — do not skip them even if you are experienced; they introduce the vocabulary and the mental model used for the rest of the course.
- Callouts tell you what kind of thinking is required. You will see four types:
- Labs are hands-on. You will build a small application called
scale-laband progressively scale it on your own laptop with Docker. Concepts you have felt — watching latency spike under load — stick far better than concepts you have only read. - Every module ends with a quiz. Click an option to lock in your answer; the explanation reveals itself immediately. Your answers and reading progress are saved in your browser, so you can close the tab and pick up where you left off.
Who this is for
You should be comfortable writing a basic web application in at least one language (examples use JavaScript/Node.js because it is the most widely read language, but every concept transfers directly to Python, Go, Java, Ruby, PHP or C#). You do not need prior experience with distributed systems, DevOps or cloud platforms — that is what we are here to learn.
0.2
Foundational Knowledge: The Life of a Web Request
Every scaling technique in this course exists to make some part of a web request faster, cheaper, or more resilient. So before we can scale
anything, we need a precise picture of what happens between a user typing https://shop.example.com/products/42 and pixels appearing on their screen.
Step 1 — DNS: turning names into addresses
Computers route packets using IP addresses (93.184.216.34, or an IPv6 address like 2606:2800:220:1::), not names. The browser
asks a recursive resolver (usually run by your ISP, or a public one like 1.1.1.1 / 8.8.8.8) to resolve the name. If the
resolver does not have the answer cached, it walks the hierarchy: a root server points it to the .com TLD server, which points it to the
domain's authoritative nameserver, which finally returns the record.
Each answer carries a TTL (time-to-live) telling caches how long they may reuse it. This matters enormously for scaling and operations: DNS is often the very first load-balancing layer (returning different IPs to different users — GeoDNS), and a long TTL means a change you make today may not reach some users for hours.
Step 2 — TCP and TLS: opening a reliable, private channel
HTTP/1.1 and HTTP/2 run over TCP, which guarantees in-order, reliable delivery. Before any data flows, TCP performs a
three-way handshake (SYN → SYN-ACK → ACK), costing one full round trip (RTT). Then TLS negotiates encryption keys:
TLS 1.3 needs one more round trip (TLS 1.2 needed two). Only then can the HTTP request be sent — another round trip for the response.
That means a brand-new HTTPS connection to a server 100 ms away costs roughly 3 × 100 ms = 300 ms before the first byte of content arrives, regardless of how fast your server is. This single fact explains three huge industries: CDNs (move the server closer, shrinking the RTT), connection reuse / keep-alive (pay the handshake once), and HTTP/3 over QUIC (runs over UDP and merges the transport and crypto handshakes into one round trip, with 0-RTT resumption for repeat visits).
Step 3–4 — Edge, load balancer and application server
In production the request rarely hits your code directly. It usually lands first on a CDN edge (serving cached static assets like images, CSS and JS from a data center near the user), then on a load balancer or reverse proxy which terminates TLS, inspects the request, and forwards it to one of many identical application servers. The application server runs your code — routing, authentication, business logic, template rendering or JSON serialization.
Step 5–6 — Cache and database
Your code almost always needs data. It first checks a fast in-memory cache (Redis, Memcached); on a miss it queries the database, the durable source of truth. Databases are the hardest part of any system to scale because they hold state — a theme we will return to constantly.
Step 7 — The response and the browser's work
The HTML response travels back (often compressed with gzip or Brotli). The browser then parses it, discovers dozens of sub-resources (CSS, JS, fonts, images), fetches them — ideally from cache or CDN — and renders. For many sites, more total time is spent in this front-end phase than on the server. Scaling is not just a backend concern.
HTTP essentials you must know cold
| Concept | What it means | Why it matters for scale |
|---|---|---|
| GET / HEAD | Read-only, safe and idempotent | Cacheable by browsers, CDNs and proxies |
| PUT / DELETE | Idempotent: repeating has the same effect as doing it once | Safe to retry automatically after a timeout |
| POST / PATCH | Not idempotent by default | Retries can double-charge — needs idempotency keys |
| 2xx / 3xx | Success / redirection (304 Not Modified) | 304s let clients reuse cached copies cheaply |
| 4xx | Client error (404, 429 Too Many Requests) | 429 is the language of rate limiting |
| 5xx | Server error (500, 502, 503, 504) | 502/504 usually mean an upstream is overloaded or dead |
| Cache-Control | Header declaring who may cache and for how long | The cheapest request is the one never made |
| Keep-Alive / HTTP/2 | Reuse one connection for many requests (HTTP/2 multiplexes them) | Avoids paying TCP+TLS handshakes repeatedly |
You can watch most of this happen yourself. Try:
# DNS: see the record and its TTL
dig +noall +answer www.wikipedia.org
# Timing breakdown of a full HTTPS request
curl -o /dev/null -s -w '
DNS lookup: %{time_namelookup}s
TCP connect: %{time_connect}s
TLS handshake: %{time_appconnect}s
First byte: %{time_starttransfer}s
Total: %{time_total}s
' https://www.wikipedia.org
0.3
Computing Fundamentals: Numbers Every Engineer Should Know
Scaling decisions are fundamentally quantitative. "Should we add a cache?" is answered by comparing the time to read from memory with the time to read from a database across a network. "Can one server handle this?" is answered by multiplying requests per second by CPU milliseconds per request. You cannot reason about any of this without a gut feel for the orders of magnitude involved.
A computer has four fundamental resources, and every bottleneck you will ever meet is one of them: CPU (computation), memory (RAM — fast, volatile, limited), storage (SSD/disk — slower, durable, large) and network (bandwidth and latency between machines). The chart below is adapted from the classic list popularized by Jeff Dean and Peter Norvig. Exact values vary by hardware generation; the ratios are what you must memorize.
What these numbers teach us
- Memory is over a million times faster than a cross-continent round trip. This is the whole justification for caching: keeping a copy of data close to where it is used.
- A network round trip inside a data center (~0.5 ms) is cheap but not free. A page that makes 200 sequential database queries spends ≥100 ms just on round trips. This is the infamous N+1 query problem.
- Sequential I/O is dramatically faster than random I/O, especially on spinning disks. Databases, log-structured storage and Kafka are all designed around this fact.
- The speed of light is a hard limit. Light in fiber travels ~200,000 km/s, so a round trip across the Atlantic can never be faster than ~60 ms. The only fix is to move data closer to users (CDNs, edge computing, multi-region deployments).
Throughput vs bandwidth vs latency
Latency is how long one thing takes. Bandwidth is the maximum capacity of a pipe (bits per second). Throughput is how much actually gets through per unit time. A highway analogy: latency is how long one car takes to drive from A to B; bandwidth is the number of lanes; throughput is how many cars actually arrive per hour (which drops during a traffic jam even though the lanes are still there).
Powers of two and time conversions
| Power | Approx. | Name |
|---|---|---|
| 210 | 1 thousand | 1 KB |
| 220 | 1 million | 1 MB |
| 230 | 1 billion | 1 GB |
| 240 | 1 trillion | 1 TB |
| 250 | 1 quadrillion | 1 PB |
| Period | Seconds | Handy rounding |
|---|---|---|
| 1 minute | 60 | — |
| 1 hour | 3,600 | — |
| 1 day | 86,400 | ≈ 105 |
| 1 month | ≈ 2.6 M | ≈ 2.5 × 106 |
| 1 year | ≈ 31.5 M | ≈ π × 107 |
Big-O, briefly
Big-O notation describes how work grows as input grows. For scaling, three shapes matter most: O(1) — a hash-table or cache lookup, constant regardless of size; O(log n) — a B-tree index lookup, where a billion rows need only ~30 comparisons; and O(n) — a full table scan, which is fine at 1,000 rows and catastrophic at 1 billion. A huge share of real-world "scaling problems" are an O(n) operation hiding inside a request that used to be fast when n was small.
0.4
Command-Line & Linux Essentials
Practically every production server runs Linux, and practically every scaling investigation starts in a terminal. You do not need to be a sysadmin, but you should be fluent with this toolkit:
| Command | What you use it for |
|---|---|
| top / htop | Live CPU and memory per process; the "load average" (runnable-queue length over 1/5/15 min) |
| free -h · vmstat 1 | Memory usage, swapping, and CPU wait for I/O (wa) |
| df -h · iostat -x 1 | Disk space; disk utilization and queue depth |
| ss -tanp · lsof -i | Open sockets and connection states (spot thousands of TIME_WAITs) |
| curl -v · dig | Inspect HTTP exchanges and DNS resolution |
| tail -f · grep · jq | Follow logs, filter them, and query JSON logs |
| ulimit -n | Max open files — every socket is a file, so this caps concurrent connections |
You should also understand processes vs threads (a process has its own memory; threads share their process's memory), and the idea of an event loop (Node.js, Nginx, Python's asyncio) where one thread juggles thousands of connections by never blocking on I/O. These execution models directly determine how a server behaves under load, and we'll analyze them in detail in Module 01.
0.5
Installation: Your Scaling Toolchain
We will install five tools. Each one has a specific job in the course:
- Git — version control for your lab code.
- Node.js (LTS) — runs the sample application. Installed via
nvmso you can switch versions freely. - Docker + Docker Compose — runs PostgreSQL, Redis, Nginx and multiple copies of our app as isolated containers, mimicking a real multi-server deployment on one laptop.
- k6 — an open-source load-testing tool (by Grafana Labs) that lets us generate thousands of requests per second and measure latency percentiles.
- jq / HTTPie (optional) — friendlier JSON and HTTP tooling.
Pick your operating system:
Install Homebrew first if you do not have it, then:
# 1. Git and helpers
brew install git jq httpie
# 2. nvm + Node.js LTS
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
exec $SHELL # reload your shell so `nvm` is on the PATH
nvm install --lts
nvm alias default 'lts/*'
# 3. Docker Desktop (includes Docker Compose v2)
brew install --cask docker
open -a Docker # start the Docker daemon once
# 4. k6 load tester
brew install k6
Apple Silicon note: all images used in this course publish ARM64 builds, so no emulation is required. In Docker Desktop → Settings → Resources, give Docker at least 4 CPUs and 6 GB RAM.
Verify everything
git --version # git version 2.4x+
node -v # an LTS version, e.g. v22.x or v24.x
docker version # both Client and Server sections must appear
docker compose version # Docker Compose version v2.x
k6 version # k6 v0.5x / v1.x
0.6
Environment Setup: Building scale-lab
Our lab is a deliberately small but realistic stack: an Express API that talks to PostgreSQL and Redis, sitting behind Nginx as a reverse proxy. It is the same shape as thousands of real production systems, shrunk to fit on a laptop. Over the course we will break it, measure it, and scale it.
Project layout
scale-lab/
├── docker-compose.yml
├── app/
│ ├── Dockerfile
│ ├── package.json
│ └── server.js
├── db/
│ └── init.sql
├── nginx/
│ └── nginx.conf
└── k6/
└── smoke.js
mkdir -p scale-lab/{app,db,nginx,k6} && cd scale-lab
cd app && npm init -y && npm install express pg redis && cd ..
app/server.js — the application
const express = require('express');
const os = require('os');
const { Pool } = require('pg');
const { createClient } = require('redis');
const app = express();
const PORT = process.env.PORT || 3000;
const INSTANCE = os.hostname(); // container ID — lets us see which replica answered
// A connection POOL: reuse a fixed number of DB connections instead of
// opening a new one per request (we'll see why this matters in Module 01).
const db = new Pool({ connectionString: process.env.DATABASE_URL, max: 10 });
const cache = createClient({ url: process.env.REDIS_URL });
cache.on('error', (err) => console.error('redis error', err));
app.get('/health', (req, res) => res.json({ ok: true, instance: INSTANCE }));
app.get('/api/hello', (req, res) => {
res.json({ message: 'hello, scale', instance: INSTANCE, at: new Date().toISOString() });
});
// Simulates CPU-heavy work (e.g. rendering, hashing, JSON transforms)
app.get('/api/cpu', (req, res) => {
const ms = Number(req.query.ms || 20);
const end = Date.now() + ms;
while (Date.now() < end) { /* busy-wait: blocks the event loop on purpose */ }
res.json({ burnedMs: ms, instance: INSTANCE });
});
// Reads from the database, with a cache-aside layer in front
app.get('/api/products/:id', async (req, res, next) => {
try {
const key = `product:${req.params.id}`;
const cached = await cache.get(key);
if (cached) return res.json({ source: 'cache', instance: INSTANCE, product: JSON.parse(cached) });
const { rows } = await db.query('SELECT id, name, price_cents FROM products WHERE id = $1', [req.params.id]);
if (!rows[0]) return res.status(404).json({ error: 'not found' });
await cache.set(key, JSON.stringify(rows[0]), { EX: 60 }); // expire after 60s
res.json({ source: 'db', instance: INSTANCE, product: rows[0] });
} catch (err) { next(err); }
});
async function main() {
await cache.connect();
app.listen(PORT, () => console.log(`scale-lab ${INSTANCE} listening on ${PORT}`));
}
main();
app/Dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
db/init.sql — seed data
CREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price_cents INTEGER NOT NULL
);
-- 100,000 rows so that missing indexes and slow queries actually hurt
INSERT INTO products (name, price_cents)
SELECT 'Product #' || g, (random() * 10000)::int
FROM generate_series(1, 100000) AS g;
nginx/nginx.conf — the reverse proxy
events { worker_connections 4096; }
http {
upstream app_servers {
# Docker's internal DNS returns one IP per replica of the "app" service.
server app:3000;
keepalive 64; # reuse upstream connections
}
server {
listen 80;
location / {
proxy_pass http://app_servers;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
docker-compose.yml — wiring it together
services:
app:
build: ./app
environment:
DATABASE_URL: postgres://lab:lab@postgres:5432/lab
REDIS_URL: redis://redis:6379
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_started }
deploy:
resources:
limits: { cpus: "1.0", memory: 256M } # make each replica a "small server"
nginx:
image: nginx:1.27-alpine
ports: ["8080:80"]
volumes: ["./nginx/nginx.conf:/etc/nginx/nginx.conf:ro"]
depends_on: [app]
postgres:
image: postgres:16-alpine
environment: { POSTGRES_USER: lab, POSTGRES_PASSWORD: lab, POSTGRES_DB: lab }
volumes: ["./db/init.sql:/docker-entrypoint-initdb.d/init.sql:ro"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U lab"]
interval: 2s
retries: 20
redis:
image: redis:7-alpine
0.7
Your First Load Test: Establishing a Baseline
You cannot improve what you have not measured. Before we change anything, we record how the system behaves today. This baseline will be the yardstick for every optimization in the course.
docker compose up -d --build
docker compose ps # all services should be "running" / "healthy"
curl -s localhost:8080/api/hello | jq # note the "instance" field
curl -s localhost:8080/api/products/42 | jq # "source": "db" the first time…
curl -s localhost:8080/api/products/42 | jq # …and "cache" the second time
k6/smoke.js
import http from 'k6/http';
import { check, sleep } from 'k6';
const BASE = __ENV.BASE_URL || 'http://localhost:8080';
export const options = {
stages: [
{ duration: '15s', target: 20 }, // ramp up to 20 virtual users
{ duration: '30s', target: 20 }, // hold
{ duration: '10s', target: 0 }, // ramp down
],
thresholds: {
http_req_failed: ['rate<0.01'], // < 1% errors
http_req_duration: ['p(95)<250'], // 95% of requests under 250 ms
},
};
export default function () {
const id = Math.floor(Math.random() * 1000) + 1;
const res = http.get(`${BASE}/api/products/${id}`);
check(res, { 'status is 200': (r) => r.status === 200 });
sleep(0.2); // "think time" between a real user's clicks
}
k6 run k6/smoke.js
k6 prints a summary. The lines to focus on:
http_req_duration....: avg=7.81ms min=1.02ms med=4.9ms max=212ms p(90)=14.2ms p(95)=21.7ms
http_req_failed......: 0.00% 0 out of 4519
http_reqs............: 4519 82.1/s
vus_max..............: 20
http_reqs/s is your throughput.med,p(90),p(95)are latency percentiles — far more meaningful thanavg, as Module 01 explains.maxis often an outlier (cold cache, JIT warm-up, garbage collection pause). Note it, but don't panic about one sample.http_req_failedmust be ~0 — a fast system that returns errors is not fast.
0.8
Core Vocabulary
These terms appear throughout the course. Skim them now; each is explored in depth later.
- Latency
- Time to complete one operation, usually measured in milliseconds and reported as percentiles.
- Throughput
- Operations completed per unit of time — requests/second (RPS), queries/second (QPS), messages/second.
- Concurrency
- Number of operations in progress at the same instant.
- Availability
- Fraction of time a system serves requests successfully, e.g. 99.9% ("three nines").
- Horizontal scaling
- Adding more machines ("scale out").
- Vertical scaling
- Making one machine bigger ("scale up").
- Stateless service
- Keeps no client data between requests in local memory, so any replica can serve any request.
- Replica
- An identical copy of a service or a dataset.
- Bottleneck
- The single most constrained resource that limits the whole system's throughput.
- SLA / SLO / SLI
- A contractual promise / an internal target / the measured indicator — covered in 1.8.
- Single point of failure (SPOF)
- A component whose failure takes the whole system down.
- Idempotency
- Performing an operation twice has the same effect as once — the prerequisite for safe retries.
Checkpoint
Module 00 Quiz
Lock in an answer to reveal the explanation. Your score is saved automatically.