Every discussion about system design eventually reduces to two numbers: latency and throughput. That sounds simple, but it is where the misunderstanding starts β€” because most of us were trained to think linearly, while any system with a queue behaves non-linearly, and unforgivingly so.

This lesson establishes four foundations reused across the following 17: telling latency apart from throughput, memorising the orders of magnitude of different delays, understanding why the mean is a deceitful number, and finally the formula that explains why a server "with 30% of its CPU idle" can nonetheless be dying.

1.1 Latency and throughput: two quantities, not one

Latency is the time to serve one request, measured in units of time. Throughput is the number of requests served per unit of time, measured in req/s. They are related but neither follows from the other, and more importantly: optimising one usually harms the other.

The clearest example is batching. Collecting 100 writes and pushing them to the database at once raises throughput sharply, because the fixed cost per write is shared out. But the first request to arrive has to wait for the other 99 before it is processed β€” its latency goes up. Compression is the same: it saves bandwidth (more throughput on the same link) but adds compress/decompress time to every request.

Technique Effect on throughput Effect on latency
Batching Large increase (fixed cost shared out) Worse (must wait for the batch to fill)
Compression Increase (fewer bytes on the same link) Worse (compress/decompress time added)
Cache Increase (less work for the tiers behind) Better (shorter path)
Adding replicas Increase, close to linear Better indirectly (less queueing, no single request runs faster)

The last row holds a subtle point that is very often misread: adding servers does not make a single request finish sooner. If a handler needs 20 ms of CPU it still needs 20 ms whether you have 1 server or 100. Extra servers only reduce queueing time. On an idle system, adding servers barely improves latency at all.

πŸ•³οΈ Pitfall: deriving latency from throughput
"The server handles 1000 req/s, so each request takes 1 ms" β€” wrong. That conclusion only holds if the server processes exactly one request at a time, sequentially. In reality, if the server runs 50 requests in parallel, each may take 50 ms while throughput is still 1000 req/s. The correct relationship between these three quantities is Little's Law in section 1.4 β€” and it needs three numbers, not two.

1.2 Latency orders of magnitude: the table that shapes every architecture

This is probably the most important table a system designer needs by heart. Not to recall each figure exactly, but to recall the distances between them β€” because those distances are what decide the architecture.

Latency orders of magnitude (log scale) 1ns 100ns 10Β΅s 1ms 100ms 10s L1 cache ~1 ns RAM ~100 ns SSD (NVMe) ~100 Β΅s RTT within one datacentre ~0.5 ms HDD seek ~10 ms RTT Hanoi ↔ Singapore ~40–60 ms Intercontinental RTT ~150 ms Default timeout Every 110 px = 100Γ—. RAM is roughly 1.5 million times faster than an intercontinental RTT.
A log scale: each 110 px step is 100Γ—. It is precisely because this scale is logarithmic that our linear intuition keeps failing.

Read this table with application in mind. RAM is roughly 1.5 million times faster than an intercontinental round trip. That single fact explains three classic architectural decisions:

  • Why caching wins almost always: turning a network query (~1 ms) into a RAM read (~100 ns) saves a factor of 10,000. No algorithmic optimisation offers a ratio like that.
  • Why CDNs exist: when the user is half a world away from the server, 150 ms is a physical limit β€” covered properly in Lesson 6. The only remedy is to move the data closer to them.
  • Why the "N+1 query" is a sin: 100 sequential queries Γ— 1 ms = 100 ms, while merging them into one query might take 3 ms. The problem is not the database but the number of network round trips.
πŸ•³οΈ Pitfall: optimising the wrong tier
Spending two days taking a function from 50 Β΅s down to 5 Β΅s, while that request is waiting on a 150 ms network round trip. You have just improved 0.03% of the total time. Before optimising anything, work out which order of magnitude the delay lives in β€” which is also why Lesson 16 (observability) exists.

1.3 Tail latency: why the mean is a deceitful number

The latency distribution of a real system is almost always right-skewed: most requests are fast, a small share are markedly slow, and that long tail drags the mean along. The result is that the mean represents nobody β€” neither the typical experience nor the worst one.

Real latency distributions are always right-skewed p50 = 20ms mean = 42ms p95 = 180ms p99 = 420ms The mean sits right of p50 and is still almost 4Γ— below p95: it describes nobody.
This shape is the default for real systems, not a pathological case. The right tail comes from GC pauses, cache misses, lock contention, retries and queueing.

So we use percentiles instead. Saying "p99 = 420 ms" means 99% of requests are faster than 420 ms and 1% are slower. For a service taking 10 million requests a day, that "1%" is 100,000 occasions of a user having a bad experience β€” every day.

Why a service's p99 becomes the user's p90

This is what makes tail latency far more dangerous than it first appears. A modern web page usually has to call several services to render one screen. If each call is independent and the probability of one call being slow is $p$, then the probability that the user hits at least one slow call when the page makes $n$ calls is:

$$P(\text{slow}) = 1 - (1 - p)^n$$

Here $p$ is the probability of a single call exceeding the slow threshold (for example $p = 0.01$ at the p99 threshold), and $n$ is the number of services the page must call. Substituting:

Services called ($n$) Probability of at least one slow call Which means
1 1.0% the service's p99 = the user's p99
10 9.6% the service's p99 β‰ˆ the user's p90
50 39.5% nearly 4 in 10 page loads are affected
100 63.4% slow becomes the normal state

This is exactly why large systems track out to p99.9, and why splitting into many services makes the tail latency problem markedly harder β€” a point we will put concrete numbers on in Lesson 15.

ℹ️ Percentiles do not add up
If a request passes through three tiers whose p99s are 10 ms, 20 ms and 30 ms, the overall p99 is not 60 ms. The reason: the tiers rarely hit their own worst case at the same moment. To know the overall p99 you have to measure across the whole path rather than summing the individual hops. For the same reason, any chart showing "the sum of the tiers' p99" deserves suspicion.
πŸ•³οΈ Pitfall: coordinated omission
Most load-testing tools work in a closed loop: each connection only sends its next request after receiving a response. Which means that when the server slows down, the tool automatically sends less β€” precisely the requests that should have landed in the worst period are never sent at all. The result: a flatteringly good p99. Lesson 2 reproduces this with the lab's own load generator so you can see how far the number is off.

1.4 Little's Law and queueing theory

Now for the part that explains most of what feels "strange" when operating systems. It starts with an equality that is surprisingly simple β€” Little's Law:

$$L = \lambda W$$

Here $L$ is the average number of requests inside the system, $\lambda$ (lambda) is the arrival rate (req/s), and $W$ is the average time each request spends inside the system (seconds). This holds for every stable system, with no assumptions at all about distributions.

A direct application: if your service takes 500 req/s and each request averages 40 ms, then the number being processed concurrently is $L = 500 \times 0.04 = 20$. That is precisely the number you need to size a connection pool or a worker count β€” not a guess.

Utilisation and the non-linear wall

Let $\mu$ (mu) be the server's maximum service rate (req/s). Utilisation is:

$$\rho = \frac{\lambda}{\mu}$$

To go further we need a concrete queueing model. The simplest is M/M/1, and those three symbols read left to right as: requests arrive randomly, service time is random, and there is one server.

ℹ️ Reading the M/M/1 notation
The first M β€” how requests arrive. A "Poisson process" sounds academic but the idea is everyday: requests arrive independently and at random, not on a schedule. An average of 10 requests per second does not mean one every 100 ms β€” some seconds bring 6, others 15. That is a good approximation of real traffic from many unrelated users.

The second M β€” service time. An "exponential distribution" means most requests are handled quickly, a few take markedly longer, and there is no upper bound. This is exactly the right-skewed shape from section 1.3.

The 1 β€” one server. Exactly one service channel. Add servers and it becomes M/M/c, and Lesson 3 measures how far adding them actually helps.

Real systems rarely satisfy all three assumptions perfectly. But M/M/1 is still worth learning because it produces the right shape of curve β€” the wall that goes vertical as ρ approaches 1 β€” and that shape is what our linear intuition keeps missing.

With that model, the average time a request spends in the system is:

$$W = \frac{1}{\mu - \lambda}$$

Look at the denominator. As $\lambda$ approaches $\mu$, the denominator approaches 0 and $W$ approaches infinity. This is not a malfunction β€” it is the normal behaviour of every queue.

Time spent WAITING in the queue, by utilisation ρ = 1 ρ=0.5 β†’ wait Γ—1 ρ=0.7 β†’ wait Γ—2.3 ρ=0.9 β†’ wait Γ—9 ρ=0.95 β†’ wait Γ—19 0 0.25 0.5 0.75 1.0 utilisation ρ = Ξ»/ΞΌ high low
The green zone (ρ < 0.6) is where linear intuition still holds. From the amber zone onwards, every extra percent of load is paid for with several percent of latency.
The markers on this chart are time waiting in the queue relative to service time, that is $\rho/(1-\rho)$ β€” not the total time in the system $W$. The two differ by exactly 1: at $\rho = 0.7$, queueing time is 2.3 times the service time, while total time in the system is 3.3 times what it is when idle.

Concrete figures for the time spent waiting in the queue (excluding service time):

Utilisation ρ Waiting time versus idle How it feels in operation
0.50 Γ—1 Comfortable
0.70 Γ—2.3 "CPU is only at 70%, plenty spare" β€” but p99 is already visibly bad
0.90 Γ—9 Shaky; small incidents become large ones
0.95 Γ—19 One modest burst and it falls over
0.99 Γ—99 Practically speaking, already dead
πŸ•³οΈ Pitfall: "CPU is only at 70%, we're fine"
This sentence is said daily in operations meetings, and it ignores the table above. At ρ = 0.7 the waiting time is already 2.3 times that of an idle system; at 0.9 it is 9 times. Worse: these are mean figures, and p99 degrades faster still. A system sitting steadily at 70% is not "30% spare" β€” it is already at the edge of the non-linear region.

And please do not "solve" this by lengthening the queue. A longer queue does not raise $\mu$ β€” it only changes the failure mode from fast rejection to wait then time out, which is usually worse for the user. Lesson 13 explains why returning a 429 immediately is the kinder answer.

Watch it happen: the single-node Traffic Lab

Reading a table is one thing; watching the curve go vertical under your own hand is another. The demo below runs a real discrete-event simulation (not a pre-rendered animation): drag the load slider and watch p99 alongside the queue length. The blue line is the theory, $W = 1/(\mu-\lambda)$, and the amber points are the simulated measurements β€” the two must agree, and that agreement is also how the series' engine is validated.

πŸ§ͺ Try it now
Pick the "1. A single server" topology, then drag the incoming load slider up from 10. Watch three marks: at 25 rps (ρ = 0.5) p99 barely moves; at 40 rps (ρ = 0.8) p99 starts separating from p50; at 50 rps (ρ = 1.0) the queue grows without bound and p99 stops converging on any value at all.

Open the Traffic Lab in its own tab β†’

The source: computing percentiles and simulating M/M/1

The two functions below are the real code running in the series' engine. The percentile function uses the "nearest-rank" method β€” the same one common load-testing tools report β€” so your numbers are comparable with theirs.

percentile_va_mm1.js
// Tinh phan vi theo phuong phap nearest-rank (khong noi suy).
// Luu y: KHONG sua doi mang dau vao - da tung co bug vi ham sort() tai cho.
function percentile(values, p) {
  if (!values || values.length === 0) return 0;
  const sorted = [...values].sort((a, b) => a - b);
  if (p <= 0) return sorted[0];
  if (p >= 100) return sorted[sorted.length - 1];
  const rank = Math.ceil((p / 100) * sorted.length);
  return sorted[Math.min(sorted.length - 1, Math.max(0, rank - 1))];
}

// Ly thuyet hang doi M/M/1. Tra ve null khi rho >= 1: hang doi tang vo han,
// KHONG co trang thai on dinh - dung tra ve mot con so gia o day.
function mm1Theory(lambda, mu) {
  const rho = lambda / mu;
  if (rho >= 1) return { rho, W: null, Wq: null, L: null, stable: false };
  const W = 1 / (mu - lambda);      // tong thoi gian trong he (giay)
  const Wq = rho / (mu - lambda);   // rieng thoi gian CHO trong hang doi
  const L = lambda * W;             // dinh luat Little
  return { rho, W, Wq, L, stable: true };
}

// Doi chieu nhanh: mu = 50 req/s (moi request 20ms)
for (const lambda of [25, 35, 45, 49]) {
  const t = mm1Theory(lambda, 50);
  console.log(`rho=${t.rho.toFixed(2)}  W=${(t.W * 1000).toFixed(1)}ms  L=${t.L.toFixed(2)}`);
}
// rho=0.50  W=40.0ms  L=1.00
// rho=0.70  W=66.7ms  L=2.33
// rho=0.90  W=200.0ms L=9.00
// rho=0.98  W=1000.0ms L=49.00   <-- chi tang lambda tu 45 len 49 ma W gap 5 lan

Those four result lines are the whole lesson of this section in miniature: raising load from 45 to 49 req/s β€” only 9% β€” takes time-in-system from 200 ms to 1000 ms, a factor of 5.

ℹ️ On how much to trust the numbers in this series
The simulator behind every demo has been checked against the M/M/1 formulas at ρ from 0.20 to 0.95, with an error under 4%; Little's Law agrees too (theoretical L of 4.000 against 3.884 measured). You can verify this yourself by downloading the source at the end of the lesson and running node sysdesign-engine-selftest.mjs β€” 42 assertions, nothing extra to install.

Every performance figure in this series comes from a real measuring machine or from the validated simulator. No number is invented, and lessons with a lab state the measuring machine's configuration explicitly.

1.5 Back-of-envelope estimation

The final skill of this lesson: turning a requirement stated in words into the numbers you need to start designing. This step comes before drawing any diagram, because without it every later choice is guesswork.

Worked through on a concrete problem: 10 million daily active users (DAU).

uoc_luong.txt
# 1) DAU -> RPS trung binh
10.000.000 DAU x 20 request/nguoi/ngay = 200.000.000 request/ngay
200.000.000 / 86.400 giay              = ~2.315 req/s   (trung binh)

# 2) He so dinh: luu luong KHONG deu trong ngay
#    Thuc te thuong dat 2-5 lan trung binh vao gio cao diem.
2.315 x 3                              = ~7.000 req/s   (dinh)

# 3) Ti le doc/ghi: dinh hinh toan bo kien truc
#    Gia dinh 100:1 (mang xa hoi, tin tuc, thuong mai dien tu deu quanh muc nay)
doc:  ~6.930 req/s   -> cache + read replica giai quyet duoc (Bai 5, Bai 7)
ghi:  ~70 req/s      -> mot primary con thua suc, CHUA can sharding (Bai 8)

# 4) Dung luong
#    Moi request ghi ~2 KB payload
70 req/s x 2 KB x 86.400              = ~12 GB/ngay
                                       = ~4,4 TB/nam  -> can nghi ve archival

# 5) Bang thong ra
#    Moi response doc ~10 KB
6.930 req/s x 10 KB                    = ~69 MB/s = ~550 Mbps  -> CDN (Bai 6)

Those five steps take under two minutes and have already eliminated a great many wrong options. Most notably: at 70 writes per second, sharding is unnecessary β€” a decision that saves months of effort and a permanent tax in complexity. Skip the estimation step and it is very easy to start with a distributed architecture for a problem one server solves.

πŸ•³οΈ Pitfall: designing for the average
The two most common mistakes at this step, both of which lead to falling over at exactly the busiest moment:

1. Forgetting the peak factor. Designing for 2,315 req/s and then taking 7,000 req/s at peak β€” the system runs at ρ β‰ˆ 3, which has no stable state at all.
2. Forgetting the read/write skew. Seeing the 7,000 req/s total and concluding "we must shard immediately", when there are only 70 writes per second. Optimising the wrong tier, paying in complexity for no benefit.
πŸ”¬ Going deeper: why a rough estimate is good enough
The purpose of a back-of-envelope calculation is not an exact figure but the right order of magnitude. Being off by 2Γ— is fine β€” the architecture for 3,500 and for 7,000 req/s is fundamentally the same. Being off by 100Γ— is another matter entirely: 70 req/s and 7,000 req/s are two completely different systems. So round aggressively, work in powers of ten, and concentrate on not getting the magnitude wrong.

One question still unanswered

This entire lesson revolves around $\mu$ β€” the maximum service rate. The tables, the curve and every estimate depend on it. But we have not said what $\mu$ actually is for your server.

That number cannot be read from documentation and cannot be guessed from the machine's specification. It has to be measured. Lesson 2 stands up a real Node app in Docker, runs increasing load, and finds the point where throughput stops rising while latency explodes β€” the wall you have just seen in theory, this time in numbers from your own machine. Along the way we will also see a handler written the wrong way collapse throughput by a factor of 195.

πŸ“– Further reading

Download the practice source

The simulation core behind every demo in the series, plus its 42-assertion self-test runnable with Node (nothing extra to install). The self-test imports the other two files, so download all three into the same directory and run node sysdesign-engine-selftest.mjs:

Download sysdesign-sim-engine.js Download sysdesign-hashring.js Download sysdesign-engine-selftest.mjs

Related lessons in this series

Lesson 2: Building the Lab & Measuring One Server's Limits Back to the System Design roadmap

Comments