Every scaling technique in this repo exists to work around one of four physical limits: CPU, memory, disk, and network. Meet them.
Prerequisites: What Is System Design? Time to read: ~20 minutes
Someone tells you “the server is slow.” That sentence is useless. A server can be slow for reasons that have nothing to do with each other:
Six completely different problems, six completely different fixes. Adding servers helps with two of them and makes one of them worse. You cannot design systems without knowing which is which.
So: what is a server actually made of?
A computer is a craftsman in a workshop.
| Part | Workshop | Speed |
|---|---|---|
| CPU registers | The tool in their hand | Instant |
| L1/L2/L3 cache | The workbench in front of them | ~1–40 ns |
| RAM | Shelves along the wall — a few steps away | ~100 ns |
| SSD | The storeroom down the hall | ~100 µs (1,000× slower than RAM) |
| HDD | A warehouse across town | ~10 ms (100,000× slower than RAM) |
| Network | Ordering from a supplier in another country | 1 ms–200 ms |
The craftsman is extremely fast — billions of operations per second — but only when the material is already on the workbench. Every trip to the storeroom stalls them completely.
This one asymmetry explains almost everything. Caching, indexing, batching, CDNs, connection pooling, columnar storage, denormalization — all of them are the same idea: stop walking to the warehouse.
The processor executes instructions. Modern server CPUs run at roughly 2–3 GHz, meaning ~3 billion cycles per second per core, with 8–128 cores.
What “CPU-bound” looks like: CPU usage near 100%, and requests get slower as concurrency increases even though the database is idle.
What actually burns CPU in a web service (in rough order of how often it’s the culprit):
📐 Numbers: a typical Python/Node service handles ~500–2,000 requests/second/core for simple work. A Go or Java service, 5,000–20,000. A tuned Nginx serving static files, 50,000+. These numbers vary by an order of magnitude — the point is the ratio, not the absolute.
Cores and parallelism. Eight cores can do eight things simultaneously. If your process is
single-threaded (classic Node.js, Python with the GIL), you use one core and the other seven
idle. That’s why you run multiple processes — cluster in Node, Gunicorn workers in Python, one per
core. → Concurrency Basics
Context switching. The OS juggles more threads than cores by switching between them. Each switch costs ~1–5 µs and trashes the CPU cache. 100 threads on 8 cores: fine. 10,000 threads: the machine spends its life switching instead of working. This is precisely why async/event-loop servers (Nginx, Node, Netty) exist — they handle 100,000 connections with a handful of threads.
Fast, volatile, limited. Server RAM ranges from 8 GB to 24 TB, but 16–128 GB is typical.
What it’s used for: your program’s heap, the OS page cache (a copy of recently read disk data — this is why the second read of a file is 1000× faster), connection buffers, and your in-process caches.
🚨 The page cache is the most under-appreciated thing in system design. When people say “Postgres is fast,” a large part of what they mean is “the working set fits in RAM, so most reads never touch the disk.” A database with 64 GB of RAM and 50 GB of hot data behaves completely differently from the same database with 8 GB of RAM. This is why the first question about any database performance problem is “does the working set fit in memory?”
When you run out:
⚖️ RAM is roughly 100× more expensive per byte than SSD, and 1000× more than object storage. “Just keep it all in memory” is a real strategy (Redis is exactly this) but you pay for it.
Persistent, slow, cheap, and where correctness lives — data isn’t durable until it’s on disk (and usually, on several disks).
HDD vs SSD — this distinction matters more than beginners expect:
| HDD | SSD (NVMe) | |
|---|---|---|
| Random read | ~10 ms | ~100 µs |
| Sequential read | ~200 MB/s | 2–7 GB/s |
| Random IOPS | ~100–200 | 100,000–1,000,000 |
| Cost/TB | Cheapest | ~4–8× HDD |
Sequential vs random is the big one. On an HDD, sequential reads are ~100× faster than random ones, because the physical head doesn’t have to move. Even on SSDs, sequential is several times faster.
This single fact shaped modern data systems:
IOPS is a hard limit. A disk that does 20,000 IOPS does 20,000 IOPS. If each request needs 5 random reads, that disk supports 4,000 requests/second. No amount of application optimization changes this — you add caching, add replicas, or buy faster disks. In the cloud, IOPS is a number you literally purchase (AWS gp3, io2), and running out of provisioned IOPS is a very common, very confusing outage.
Durability isn’t free. write() puts data in the OS page cache and returns — fast, but the data
is gone if the machine loses power. fsync() forces it to physical storage: 0.1–10 ms. Every
database chooses where on this spectrum to sit, and that choice is exactly the durability/latency
trade-off.
The one you’ll think about most, because in a distributed system nearly all the time is spent here.
Bandwidth — how much data per second. A typical cloud VM has 1–25 Gbps.
📐 At 1 Gbps you can push ~125 MB/s. Serving a 2 MB image means at most ~62 images/second from that machine. That number surprises people, and it’s why you serve images from a CDN, not from your app servers.
Latency — how long one round trip takes. And this is dominated by physics, not by your code:
| Route | Round-trip |
|---|---|
| Same machine (localhost) | ~0.05 ms |
| Same datacenter | ~0.5 ms |
| Same region, different AZ | ~1–2 ms |
| Karachi → Dubai | ~30 ms |
| Karachi → Frankfurt | ~110 ms |
| Karachi → Virginia (US East) | ~230 ms |
| Anywhere → anywhere, worst case | ~300 ms |
Light travels 300,000 km/s in vacuum, ~200,000 km/s in fibre, and cables don’t run in straight lines. You cannot optimize your way past the speed of light. If your user is in Karachi and your server is in Virginia, every round trip costs ~230 ms, forever. The only fixes are: make fewer round trips, or move the server closer (→ CDN, Multi-Region).
🚨 This is why “chatty” APIs are a design smell. An endpoint that makes 10 sequential internal calls of 1 ms each costs 10 ms. The same 10 calls across regions cost 2.3 seconds.
Connections cost resources. Each TCP connection uses a file descriptor and kernel buffers (~10 KB of memory). Default file descriptor limits (often 1024) will stop your server long before RAM does. Establishing a connection costs a round trip; TLS costs one or two more. This is why connection pooling and keep-alive exist. → Connection Pooling
A “simple” API request — GET /users/42 — on a well-behaved service:
Client → Load Balancer 0.5 ms network
LB → App server 0.5 ms network
App: parse request 0.1 ms CPU
App → Cache (Redis) lookup 0.5 ms network + ~0.1 ms Redis work
↳ MISS
App → Database query 1.0 ms network
Database: index lookup 0.2 ms CPU + page cache
Database: read row 0.1 ms page cache hit (or 0.1 ms SSD)
Database: serialize result 0.3 ms CPU
App: build JSON response 0.5 ms CPU ← often the biggest single item!
App → Client 1.0 ms network
────────
~4.8 ms
Two lessons that show up constantly:
Now add a user in Karachi hitting a server in Virginia: add 230 ms to the first and last lines. Your 4.8 ms service is a 235 ms experience. Your code was never the problem.
| Technique | Buys you | Costs you |
|---|---|---|
| Cache in RAM | 1000× faster reads | Staleness, memory cost, invalidation complexity |
| Sequential writes (append log) | 100× write throughput | Reads need indexes/compaction |
| Batching | Fewer round trips, better throughput | Higher latency for the individual item |
| More threads | More concurrency | Context-switch overhead, memory per thread |
| Async I/O | 100k connections on few threads | Harder code, one blocking call ruins everything |
fsync on every write |
Real durability | 0.1–10 ms per write |
Measure your own machine. Write a small script that times:
dd to create a 20 GB file).https://example.com.Then compare your numbers to Latency Numbers. Two things will surprise you: how enormous the random-vs-sequential gap is, and how completely the network dominates everything else.
Bonus: run htop while your machine does something heavy. Watch which resource actually saturates.