Here is a question that looks like arithmetic and behaves like a trap:
A ride-hailing platform has 200,000 drivers online. Each phone reports GPS every 4 seconds, about 500 bytes per report. What does the backend deal with?
Nothing in it is hard. It’s division and multiplication with numbers a ten-year-old could handle. And yet this exact shape of question — translate a product description into load, then into a verdict — is where competent engineers freeze. Not because they can’t compute it, but because they’ve never practiced computing it out loud, under time pressure, with the units attached.
I froze on it too. This series is what I learned by going back and doing the drills properly. Part 1 is the foundation everything else stands on: turning a sentence about users into numbers about machines, without losing a factor of 1,000 along the way.
The four-step method#
Every capacity question — messages, storage, connections, servers — reduces to the same little machine:
| Step | The move | Why it’s there |
|---|---|---|
| 1 | Compute the load | Events per second, bytes per second, bytes per day. |
| 2 | Divide by a machine’s ceiling | Needs a small vocabulary of memorized ceilings — Part 2 of this series is that list. |
| 3 | Add headroom (× 1.5) | Never run anything above 60–70% of its ceiling; queueing theory punishes the last 30% brutally. |
| 4 | Sanity-check the shape | 4,000 database servers for a to-do app doesn’t mean the method failed — it means an input was wrong, and noticing that is the skill. |
Steps 2–4 need those memorized constants, so this post drills step 1 until it’s reflexive — because step 1 is where the silent, embarrassing errors live. They all involve units.
Count the zeros, not the words#
The single most common failure in load math is not a wrong formula. It’s skipping a rung on the unit ladder — jumping from bytes straight to gigabytes and being off by a factor of 1,000 while every digit in the answer looks right.
The fix is to stop thinking in unit names and start thinking in powers of ten:
| Zeros | Power | Unit | Feel for it |
|---|---|---|---|
| — | ⅛ byte | bit | what network links are quoted in — 8 bits = 1 byte |
| 0 | B | one character | |
| 3 | KB | a paragraph of JSON | |
| 6 | MB | a photo | |
| 9 | GB | a movie | |
| 12 | TB | a laptop disk | |
| 15 | PB | “we are now a data company” |
Worked the safe way: 10,000 messages/second × 200 bytes = 2,000,000 bytes/second. Don’t reach for a unit yet — count the zeros first: . Six zeros is mega. That’s 2 MB/s. The dangerous way is to see “2,000,000” and think “2,000 thousands… 2 GB?” — one skipped rung, three orders of magnitude, and the rest of your design is now sized for a system 1,000× bigger than reality.
Two supporting habits:
- Always promote to the largest unit that keeps the number ≥ 1. Say 2.5 TB, never 2,500 GB — the same way you’d say $25, never 2,500 cents. Unpromoted units read like you don’t own the number, and worse, they hide the magnitude from you.
- 1,000 ≈ 1,024 always. Napkin math that distinguishes GiB from GB isn’t rigorous, it’s slow. Round everything to one significant digit and narrate the rounding.
- In log space, × is + and ÷ is −. For division-heavy problems, zeros beat digits: “800K senders × 12 Mbps each ÷ 1 Gbps links” is links — three additions, no long division. Same ladder, worked as exponents.
- Keep the units — “they’re good checksumming” (Simon Eskildsen’s napkin-math line). If the units of your answer don’t come out as the thing you were asked for, the mistake announces itself before the number does.
Bits vs bytes: multiply by 8 exactly once#
Data at rest is measured in bytes (files, messages, disks). Links are quoted in bits per second (a “10 Gbps NIC” moves 10 gigabits, which is only 1.25 gigabytes, each second). Mixing them up is an error of 8× — small enough to survive a first glance, big enough to wreck a design.
The discipline that makes it impossible to double-apply or forget: do the ×8 exactly once, at the moment of comparison, and say it out loud. Compute everything in bytes, and only when the question becomes “does this fit through the pipe?” convert: 25 MB/s × 8 = 200 Mbit/s, against a 10,000 Mbit/s NIC → 2%.
And do that last division honestly. 0.2 Gbps out of 10 Gbps is 2%, not 20% — when the numbers are this easy it’s tempting to eyeball the percentage, and eyeballing is exactly how a non-issue gets reported as a concern (or the reverse). Write the fraction, divide it, then speak.
One honesty footnote for when precision matters: napkin bandwidth uses payload size, but the wire carries framing too — Ethernet + IP + TCP headers are ~60–100 bytes, plus TLS and WebSocket/HTTP overhead on top. For megabyte blobs that’s noise; for a 200-byte message it can approach half the wire traffic. The napkin answer is still right at napkin precision — just know that tiny-message systems run maybe 1.5× hotter on the wire than the payload math says, and say so if the estimate is close to a ceiling.
The 100,000-second day#
A day has 86,400 seconds. For napkin purposes, use 100,000 (): the error is 16%, which is nothing against numbers you’ve already rounded to one digit, and it turns per-day volume into a zero-shifting exercise instead of long multiplication:
This isn’t a private shortcut — it’s standard practice, and the strongest version of it is naming it as you go: “call a day 100K seconds — that overestimates by about 16%, which is fine at this precision.” Announcing your rounding is what separates estimation from guessing.
The year has an even better-known companion shortcut, courtesy of Tom Duff at Bell Labs: “π seconds is a nanocentury.” A century is close to seconds, so a year is about — call it , or just “30 million seconds” — and yearly volumes become the same zero-shifting game. (Jon Bentley made the line famous in Programming Pearls, the book that invented this whole napkin genre.)
One trap to watch as you do it: the multiplication consumes the seconds. You walked in with megabytes per second and walked out with terabytes per day — restate the new unit the moment it changes hands, or an hour later you’ll catch yourself saying “2.5 TB per second” and quietly dying inside. After every multiplication, say the full unit.
From DAU to QPS — the same shortcut in reverse#
Two acronyms carry most capacity conversations, so let’s pin them down properly:
- DAU — daily active users: how many distinct people use the product in a day. This is the number product people quote, because it’s how businesses measure themselves (“Instagram has ~500M DAU”). Notice what it doesn’t tell you: nothing about how often each user actually does anything.
- QPS — queries per second (you’ll also see RPS, requests per second — same idea): how many requests hit your servers each second. This is the number engineers need, because machines are sized in per-second capacity, not per-day users.
Nearly every capacity question hands you the first and needs the second. The bridge is one extra fact you must ask for or assume out loud — actions per user per day — and then the 100K-second day runs in reverse:
Any daily total becomes a per-second rate by knocking five zeros off it — that’s the whole conversion. It also mints a pocket anchor worth carrying whole: 1 million requests/day ≈ 12/sec (≈10/sec with the shortcut). Held next to real-world scale — Google runs on the order of 100K searches/sec — that one anchor instantly places any product someone describes to you.
Two interpretation notes matter as much as the arithmetic.
First: that 10K QPS is an average, and the average quietly assumes people act at 3 a.m. Dividing by the whole day spreads traffic evenly across all 24 hours — but humans sleep, so the real traffic squeezes into the waking hours. The peak factor isn’t a magic constant; it’s the same division done honestly. Toy version: 24 cookies over 24 hours is 1/hour, but the same 24 cookies eaten in 8 hours is 3/hour — squeeze the same total into a third of the time and the rate triples. Applied: if ~80% of the day’s actions land in the ~8 busy hours (≈ 30K seconds), the busy-hour rate is 27K QPS — roughly 2.5–3× the average. So the quotable line is: “10K QPS average, ~3× in busy hours, so I provision for ~30K.” (Rarer synchronized spikes — New Year’s midnight, breaking news — run 10×+ and get handled as planned events with pre-scaling and buffering, not year-round hardware; the peak-vs-average section below digs into when averages can and can’t be trusted.)
Second: split reads from writes before sizing anything — and derive the ratio from the product, don’t memorize one. Every product mechanically implies its own ratio: a tweet is written once and read by thousands (~1000:1, read-dominated); a chat message is written once and read by the ~5 people in the conversation (1:5, write-heavy); a GPS ping is written once and maybe never read at all. So “10K QPS” is meaningless until you split it — and the split is the answer to “what does one user action cause?” The two halves then get compared against different machine ceilings: reads scale out easily with caches and replicas, while every write lands on the one primary database. That read/write split is where the database sizing in Part 3 begins.
Try it: the whole chain, live#
Here’s the complete four-step chain on the ride-hailing numbers — and on any others you want to try. Watch what each step does to the units, and watch the verdicts flip as you drag the sliders:
For the baseline scenario: 200,000 ÷ 4 = 50,000 messages/second, × 500 B = 25 MB/s, ×8 = 200 Mbit = 2% of one NIC, × 100K = 2.5 TB/day. Four numbers, maybe forty seconds of talking. The rest of this post is about what those four numbers mean.
One load, two ceilings#
Notice the calculator computes two different rates — messages per second and bytes per second — and never merges them. That’s deliberate, and it’s a distinction I wish someone had drawn for me years earlier: every message costs you twice — once per message, and once per byte.
- Per-message cost, paid regardless of size: a syscall to read from the socket (a user→kernel context switch), parsing, allocating, updating state. A million tiny messages a second will melt your CPU while the NIC sits at 4% — the network yawns while the kernel drowns.
- Per-byte cost, paid regardless of count: the bytes actually crossing the wire and being copied through buffers. A hundred huge blobs a second is nothing to parse but can saturate the pipe before the CPU wakes up.
Deep dive: what one tiny message actually costs the kernel — syscalls, copies, and why HFT firms bypass the kernel entirely
Follow a single 200-byte market tick from the wire into your process, and the per-message tax stops being abstract:
- ① The NIC drops the packet into memory. The network card DMAs it straight into a kernel ring buffer — no CPU involved. This step is nearly free, which is exactly why the network side scales so much better than everything after it.
- ② A core gets interrupted. Classically one interrupt per packet; at high rates Linux polls packets off the ring in batches (NAPI) precisely because interrupt-per-packet would flatten the machine. Either way, a core abandons what it was doing — pipeline drained, caches disturbed.
- ③ The TCP/IP stack does its per-packet ritual: verify the checksum, update sequence state, generate an ACK, append to the socket’s receive buffer. Microseconds of pointer-chasing through structures built for generality, not speed.
- ④ Your process gets woken up: an epoll wakeup, a scheduler decision, maybe a migration to a different core — which throws away the warm caches again.
- ⑤ You call
recv()— the syscall. A mode switch from user space to kernel space and back, roughly a microsecond round-trip (worse since Spectre/Meltdown mitigations added flushing at the boundary). Useful application work done during it: zero. - ⑥ The kernel copies the bytes into your buffer. For 200 bytes the copy itself is trivial — the ceremony around it is the cost.
- ⑦ Your actual work finally begins: parse, allocate, lock, update state.
Steps ②–⑥ cost roughly the same whether the payload is 200 bytes or 20,000 — that is what “per-message cost” means. Now multiply: at 1M messages/second, even a conservative ~2 µs of per-message ceremony is full CPU-seconds of work per wall-clock second — two entire cores consumed before a single byte is parsed. Meanwhile 1M × 200 B = 200 MB/s ≈ 1.6 Gbps: the NIC is at 16%, dozing. The machine is dying while the bandwidth graph insists everything is fine.
This is also why the mitigation ladder looks the way it does — every rung amortizes the ceremony, never the bytes:
- Batch the syscall:
recvmmsg()pulls dozens of packets per mode switch;io_uringgoes further and turns submission/completion into shared-memory queues, so steady-state IO needs almost no syscalls at all. - Cheapen the parse: binary formats (fixed-offset protocols like an exchange’s ITCH feed) turn parsing from allocation-heavy JSON walking into a pointer cast.
- Pin and poll: dedicate a core that busy-spins on the socket instead of sleeping — burning 100% CPU to delete step 4’s wakeup latency and its cache-refill aftermath.
- Bypass the kernel entirely: DPDK, AF_XDP, or Onload-style stacks map the NIC’s rings directly into user space — packets go NIC → your memory, and steps ②–⑥ cease to exist. No interrupts, no syscalls, no copies, no kernel TCP stack (you bring your own). This is the HFT endgame, and the trade is stark: sub-microsecond receive paths, paid for with a dedicated spinning core, custom networking code, and the loss of every convenience the kernel was quietly providing.
The napkin-math takeaway survives even if the details fade: a machine’s message-rate ceiling is set by fixed per-message ceremony, not payload size — tiny-message systems hit it absurdly early, and every fix amounts to performing the ceremony less often, not shrinking the messages.
This is why the two rates get computed separately and checked separately. Tiny-message systems (market data, telemetry, chat) end up engineered around syscall reduction — reading many messages per syscall, binary encodings instead of JSON, kernel bypass at the extreme. Big-blob systems (video, backups) end up engineered around the pipe — compression, CDNs, multiple NICs. Same word, “load”; completely different medicine.
The one-liner worth internalizing: tiny-message systems die on CPU long before the network is busy; big-blob systems die on the NIC while the CPU idles.
Peak vs average: when is the mean a lie?#
Back to the drivers. 200,000 phones ÷ 4 seconds = 50,000 messages/second — but hold on. That’s an average. What if all 200,000 phones happen to send in the same instant? Shouldn’t we size for 200,000/second, a 4× bigger system?
This objection feels rigorous and is usually wrong, and the reason why is one of the most useful facts in capacity planning: independent senders smooth themselves out. Each phone started its 4-second timer when it came online — effectively at a random moment. With 200,000 uncoordinated timers, the law of large numbers takes over, and arrivals in any given slice of time hug the average with a wobble of a fraction of a percent. The nightmare scenario of everyone-at-once isn’t just unlikely — with independent senders, it’s statistically unreachable.
So when does the average lie? When the senders stop being independent — and here’s the uncomfortable part: in real systems, synchronization is almost never natural. It’s built, accidentally, by us:
- A firmware update makes every phone report on the wall clock — at :00, :04, :08 — instead of on its own timer. Congratulations: 200,000 requests in the same 50 ms, silence for 3.95 s, repeat.
- A push notification goes out and every app wakes up and phones home at once.
- A server restart drops all clients, and every client reconnects immediately — the thundering herd. The recovery traffic dwarfs anything normal operation produces.
- A nightly cron at midnight in each timezone marches a synchronized wave around the planet, once an hour.
Watch the difference — it’s more dramatic than any paragraph can make it:
The rule that falls out: independent senders average out; anything that shares a clock or a trigger synchronizes. And its constructive twin — jitter anything that could self-synchronize. Jitter just means adding a small random delay: each phone offsets its schedule by a random fraction of a second; each reconnecting client sleeps a random amount before dialing back. It costs one line of code and converts a design-made spike back into the smooth traffic the averages promised. (This is the same medicine as the randomized backoff in every well-built reconnect loop — the herd is the same herd.)
So the defensible way to state a load estimate is the assumption plus the number: “assuming senders are on independent timers, arrivals smooth out and I’ll size for the 50K/second average, with a 2–3× peak factor for provisioning — and I’d jitter anything clock-driven so we never manufacture a spike ourselves.” One sentence carries the average, the awareness of bursts, and the mitigation. (Some traffic genuinely is correlated — market data during breaking news, everyone in a chat app reacting to the same event. That deserves a bigger peak factor, and a later part of this series.)
The peak factor itself has three industry shorthands, and it’s reassuring how tightly they agree: “peak = 2× average” (Alex Xu’s rule in the standard estimation text), “10% of a day’s traffic lands in the peak hour” (which works out to 2.4×), and “80% of traffic in 20% of the day” (≈4×). All three are the cookie arithmetic wearing different clothes — quote whichever you like, but knowing why they’re all in the same range is what makes the number yours.
The verdict step: how fast does the data lose value?#
The chain ends with 2.5 TB/day, and here’s where a subtle skill kicks in: the per-second and per-day numbers answer different questions, and each needs its own verdict.
Per-second answers “can we ingest it?” 50K messages/second at 2% of one NIC — yes, trivially. Boring, and saying “boring” out loud with the numbers to back it is a feature, not a shrug.
Per-day answers “where does it go, and for how long?” 2.5 TB/day is where it stops being boring: that’s roughly a petabyte a year. No single boring database survives that — you’d fill a large disk weekly while every index amplifies every write.
The senior move here is to stop asking “is it big?” and ask “how fast does its value decay?” GPS pings have a brutally steep decay curve, and the storage design should follow the curve down:
- The last ping per driver is gold — the live map, the ETA, the dispatch decision. But “latest position for 200K drivers” is only 200K × 500 B ≈ 100 MB. That fits in memory on your laptop. Overwrite in place; history not required.
- Recent history earns its keep for hours to weeks — the trip route, the fare calculation, the support dispute. Append it to cheap columnar storage partitioned by day, and expire it by dropping whole partitions (which is free) rather than deleting rows (which is not).
- Old raw pings are individually worthless. Nobody asks where driver 4711 was at 3:07 pm eight months ago. Downsample — one ping a minute instead of one per 4 seconds is 15× smaller — aggregate into trips, and let the raw data go.
The complete verdict: “Ingest is boring — 2% of a NIC. Storage is the real question: 2.5 TB/day is a petabyte a year, so split it by value — latest positions in memory, recent raw pings in partitioned columnar storage with a short TTL, downsampled summaries as the only thing kept forever. Raw GPS depreciates in minutes; the design should too.”
That question — how fast does the value decay? — generalizes to everything. Metrics, clickstreams, and market ticks decay fast: tier them, expire them. Orders, payments, and anything with money attached decays never: that’s what the durable, boring, transactional database is for. Most real systems are both at once, and drawing that line early is half the architecture.
The habits, collected#
Everything above compresses to six reflexes. Treat this as a literal checklist — run it against the next capacity question you meet, and don’t stop practicing until every box feels boring:
- LOAD = rate × size, then divide by a ceiling, then add headroom, then sanity-check the shape.
- Count zeros, not unit names — and promote to the largest unit ≥ 1 (2.5 TB, never 2,500 GB).
- ×8 exactly once, at the moment you compare bytes to a link — and do the final division on paper, not by eyeball.
- A day is 100K seconds. Say the ~16% rounding out loud; restate the unit after every multiplication.
- Check both ceilings: event rate against the CPU, byte rate against the NIC. They fail independently and want different fixes.
- Trust averages from independent senders; distrust anything that shares a clock — and jitter whatever could self-synchronize.
Part 2 covers the other half of the machine: the numbers that step 2 divides by. That means the classic latency ladder — L1 cache to RAM to SSD to a datacenter hop to a cross-region round trip, each rung roughly 100× the last — and the throughput ceilings it explains: what one database node, one cache, one WebSocket server, and one queue can take before falling over. The ladder is the more powerful half: memorize five latencies and you can derive ceilings you never memorized.
Comments
Signed in with GitHub. Be kind.