Module 2: Napkin Estimation (Back-of-the-Envelope)
7. Smart rounding and sanity checks
Description
You already have Enlace's four numbers —QPS, storage, bandwidth, memory—, each computed with its calculation. But a computed number isn't yet a trustworthy number: anyone can mistype a zero, drag an extra decimal, or confuse a unit, and come out with a figure that looks rigorous and is a thousand times wrong. This lesson teaches the discipline that turns a pile of calculations into an estimate you can trust and that withstands questions: smart rounding and sanity checks.
They're two habits that go together. Smart rounding is reporting each number with the precision you actually have —one significant figure, thinking in powers of 10— and not faking decimals you don't know. Sanity checks are the quick verifications that confirm a number isn't nonsense: crossing two different paths to reach the same result, comparing it against known references ("the numbers every programmer should know"), and applying the smell test —the instinct that says "this can't be"— when a figure falls in the wrong mark. By the end you'll know how to round like a mature estimator and detect an impossible number before it contaminates a whole design.
Connection to the module: this is the discipline-lesson. It doesn't compute a new Enlace number; it takes the four you already produced and applies the quality filter that makes them defensible (the keyword of lesson 1). The rounding formalizes what you'd been doing by hand since lesson 1 (38.58 → ~40); the sanity checks are the safety net you'll use in lesson 8, when you assemble the complete capacity table and need to trust each row. Think of it as the "check your work" step that separates a rushed calculation from a professional estimate.
The restaurant bill
Imagine you go out to dinner with three friends and the bill arrives: $1,240. Before paying, you do something automatic in two seconds, without recalculating each dish: you think "we're four, we each ordered a ~$200 dish and an ~$80 drink, roughly $280 a head, times four is ~$1,120... yes, $1,240 with the tip checks out". You didn't verify the exact sum line by line; you did an order-of-magnitude check: does the total fall where it should for what we ordered? If the bill said $12,400, you'd flinch immediately —"impossible, we ate, we didn't buy the restaurant"— even without knowing exactly where the error is. And if it said $124, you'd also be suspicious —"too cheap for four people"—. Your instinct places the total on the correct mark (thousands, not tens of thousands or hundreds), and with that you detect the nonsense without auditing each line.
That's a sanity check, and notice the three things you did, because they hold for any systems estimate:
- You rounded to be able to think. You didn't use "$198.50 a dish"; you used "~$200". The round numbers let you do the calculation in your head. The false precision (the .50) would have gotten in the way without adding anything.
- You crossed two paths. You compared the printed total ($1,240) with your independent estimate (~$1,120 + tip). Two routes to the same number; if they agree on the mark, you trust; if not, you investigate.
- You used known references. "A dish costs ~$200" is a number you carry in your memory, your reference class. Without it you couldn't judge whether the total makes sense.
Estimating systems is the same discipline with other numbers. A mature engineer never drops a figure without this reflex: rounding it to be able to reason about it, crossing it with a second path, and comparing it against what they know is normal. This lesson turns that reflex into a method.
Part 1: smart rounding
One significant figure
The central rule of napkin rounding is brutal and liberating: report a single significant figure. A significant figure is the first non-zero digit; everything else is noise that fakes precision. 38.58 has one useful significant figure (the 4 of ~40, after rounding); you don't know the .58. 6.144 TB is reported as ~6 TB. 1,929,012 B/s is reported as ~2 MB/s.
Why a single figure? Because your input data has one figure. "100 million URLs a month" isn't exactly 100,000,000: it's "on the order of a hundred million", with an enormous margin (and in December?, and if it grows?). If the input has one figure of precision, the output can't have three: reporting 38.58/s from "~100M/month" is inventing precision that never existed. The golden rule of uncertainty propagation: the result can't be more precise than the most imprecise of its inputs. And all your napkin inputs are one figure.
Let's run the rounding to one significant figure over the module's raw numbers, to see the discipline in action:
from math import log10, floor
def one_sig(x):
# Rounds to ONE significant figure.
if x == 0:
return 0
d = floor(log10(abs(x))) # the order of magnitude (the power of 10)
return round(x, -d)
raw = {
"qps_write (100M/2.592M s)": 38.58,
"qps_read (x100)": 3858,
"read bandwidth (B/s)": 1_929_012,
"storage 5 years (B)": 6.144e12,
"working set (B)": 333_333_333,
}
for name, value in raw.items():
print(f"{name:<32} {value:>16,.0f} -> {one_sig(value):>16,.0f}")
What to expect.
qps_write (100M/2.592M s) 39 -> 40
qps_read (x100) 3,858 -> 4,000
read bandwidth (B/s) 1,929,012 -> 2,000,000
storage 5 years (B) 6,144,000,000,000 -> 6,000,000,000,000
working set (B) 333,333,333 -> 300,000,000
Each raw number collapses to one digit times a power of 10: ~40, ~4,000, ~2 MB/s, ~6 TB, ~3×10⁸ B. Notice the last: 333,333,333 rounds to 300,000,000 (3×10⁸) because the first digit is 3; in practice we report it as "~333 MB" or "hundreds of MB" —the one-figure rounding says "3×10⁸", which is the mark, and "333 MB" keeps a bit more for the table, both honest—. The discipline isn't that the number be ugly, but that it communicates how much you know: ~6 TB says "a few terabytes, 10¹² mark", which is exactly what your calculation justifies.
Round from the start, not at the end
The second habit: round on the way in, not on the way out. Dragging 2,592,000 seconds per month through three steps and rounding only the final result is wasted work and an invitation to a typing error. Round before operating: "a month is ~2.6 million seconds" (or even ~2.5 × 10⁶ if you want to round more), and operate with that. As you saw in lesson 2, rounding the constants (30 days, base 10, 86,400 → 10⁵) moves the result by a small percentage that disappears in the final rounding. Operating with round numbers from the start is what lets you do the calculation in your head, which is the whole point of the napkin.
There's a hygiene exception you already saw in lesson 3: when a number feeds another (chaining), it's best to drag the raw one more step before rounding, so as not to accumulate the rounding error. That's why lesson 3 computed qps_read = 38.58 × 100 (raw) and rounded at the end to ~4,000, instead of 40 × 100 = 4,000 —they give almost the same, but dragging the raw is cleaner—. The combined rule: round the prompt's inputs from the start; drag intermediate results without rounding when they feed another calculation; round to one figure when reporting.
Thinking in powers of 10
Smart rounding and powers of 10 (lesson 2) are the same discipline seen two ways. Rounding to one significant figure is writing the number as digit × 10^n. ~6 TB is 6 × 10¹²; ~4,000/s is 4 × 10³; ~333 MB is ~3 × 10⁸. Thinking this way has an advantage for the sanity check to come: when a number is in scientific notation, comparing two numbers is comparing their exponents (their marks), and an error of "a thousand times" jumps out as a difference of 3 in the exponent. 6 × 10¹² against 6 × 10¹⁵ —6 TB against 6 PB— is an exponent difference of 3, a factor of a thousand, impossible to overlook if you think in marks. Rounding to powers of 10 isn't just aesthetic: it's what makes mark errors visible.
Part 2: the sanity checks
Rounding gives you clean numbers; the sanity checks tell you whether those numbers are credible. There are four techniques, and a good estimator applies at least one to each important figure before trusting it.
Technique 1: cross two independent paths
The most powerful verification is reaching the same number by two different routes. If two independent paths land on the same mark, it's very unlikely both have the same error; the number is solid. If they diverge, there's a bug in one of the two and you found it before it did harm. Let's test it with three numbers from the module:
# PATH A vs PATH B for three Enlace numbers.
# 1) Write QPS: divide at once vs go down the time staircase
a1 = 100_000_000 / 2_592_000 # ÷ seconds/month at once
b1 = 100_000_000 / 30 / 86_400 # ÷30 (days) then ÷86400 (sec/day)
print(f"qps_write A={a1:.2f}/s B={b1:.2f}/s")
# 2) Reads per day: from the QPS vs from the monthly total
a2 = 4000 * 86_400 # 4000/s x seconds/day
b2 = 100_000_000 * 100 / 30 # (100M x100 reads) / 30 days
print(f"reads/day A={a2:,.0f} B={b2:,.0f} (same mark ~3.x x10^8)")
# 3) Storage at 5 years: by total records vs by months
a3 = 6e9 * 1000 # 6 billion records x 1KB
b3 = 100_000_000 * 60 * 1000 # 100M/month x 60 months x 1KB
print(f"storage A={a3/1e12:.0f}TB B={b3/1e12:.0f}TB")
What to expect.
qps_write A=38.58/s B=38.58/s
reads/day A=345,600,000 B=333,333,333 (same mark ~3.x x10^8)
reads/day ...
storage A=6TB B=6TB
All three pass. The write QPS gives identical values by the two paths (it was the same division broken down). The storage, identical. And the reads per day give 345.6M by one path and 333.3M by the other —they're not equal, but they fall on the same mark (~3×10⁸), and the small difference (a ~3.7%) has a known explanation: it comes from rounding 38.58 to 40 in path A. A mature estimator recognizes that difference as expected (they know where it comes from) and isn't alarmed: both numbers say "on the order of three-hundred-and-something million reads a day", which is the conclusion. When two paths differ, the question isn't "which is the correct one?" but "does the difference fall within what my rounding explains, or does it reveal a real error?". A 3.7% is explained by the rounding; a factor of 10 would be a bug.
Technique 2: compare against known references
A number in a vacuum can't be judged; against a reference, it can. Veteran estimators carry in their head a handful of reference numbers —sizes, latencies, typical capacities— against which they contrast their results. "4,000 reads/s" says nothing until you know that "a modest database does thousands of reads/s" (then: loadable, but it has to be watched) and that "a giant does millions/s" (then: Enlace is very far from that). The reference is what turns a number into a judgment.
The most famous reference collection is the table of latencies every programmer should know (attributed to Jeff Dean, updated by others). You don't have to memorize it to the digit —they're orders of magnitude that change with the hardware and the year—, but the proportions between layers are stable and very valuable. Let's compute them instead of quoting them:
# Reference latencies (ORDERS OF MAGNITUDE, not exact values; vary by hardware/year).
mem_ns = 100 # read from memory (RAM) ~100 nanoseconds
ssd_us = 100 # random read from SSD ~100 microseconds
disk_ms = 10 # hard disk seek ~10 milliseconds
print(f"memory (RAM): ~{mem_ns} ns")
print(f"SSD (random): ~{ssd_us} us = {ssd_us*1e3:.0f} ns")
print(f"disk (seek): ~{disk_ms} ms = {disk_ms*1e6:.0f} ns")
print(f"disk / memory = {disk_ms*1e6/mem_ns:,.0f}x slower")
print(f"SSD / memory = {ssd_us*1e3/mem_ns:,.0f}x slower")
dc_ms, cross_ms = 0.5, 150
print(f"round trip same datacenter ~{dc_ms} ms | cross continent ~{cross_ms} ms ({cross_ms/dc_ms:.0f}x)")
What to expect.
memory (RAM): ~100 ns
SSD (random): ~100 us = 100000 ns
disk (seek): ~10 ms = 10000000 ns
disk / memory = 100,000x slower
SSD / memory = 1,000x slower
round trip same datacenter ~0.5 ms | cross continent ~150 ms (300x)
These proportions are gold for the sanity checks. That memory is ~100,000 times faster than disk is the reference that confirms, at a glance, why the working set from lesson 6 is worth it: moving the hot reads from disk (10 ms) to RAM (100 ns) speeds them up five orders of magnitude. That crossing a continent is ~300 times slower than a local round trip is the reference that, later in the guide, will justify putting servers close to the users. You don't need the exact nanoseconds; you need to know that memory ≪ SSD ≪ disk ≪ far-network, each layer one or two orders of magnitude above the next. With that hierarchy in your head, many numbers are validated (or discarded) at a glance.
Other size references worth having, in the same spirit:
| Thing | Reference size |
|---|---|
| A character (ASCII) | 1 byte |
| A URL / a line of text | ~hundreds of bytes (Enlace: ~500 B) |
| A textbook (text only) | ~1–5 MB |
| A photo (compressed) | ~1–5 MB |
| A high-definition movie | ~1–5 GB |
| Seconds in a day | ~10⁵ (86,400) |
| Seconds in a year | ~3 × 10⁷ (≈ π × 10⁷) |
Against these references, "an Enlace record weighs ~1 KB" is validated instantly (a URL is hundreds of bytes, plus metadata, ~1 KB checks out), and "Enlace stores 6 TB" too (6 billion records of ~1 KB = 6 TB, and 6 TB is "a few disks", nothing monstrous). A number that clashes with these references —"each URL weighs 5 MB", "Enlace needs 6 PB"— sets off the alarm without further analysis.
Technique 3: the smell test (detecting the impossible)
The smell test is the restaurant-bill reflex: looking at a number and immediately feeling that "this can't be", because it falls in an absurd mark for what it represents. It doesn't prove a number is correct, but it catches the gross errors —the factor-1,000 ones, typical of an extra zero or a confused unit— which are exactly the ones that ruin a design. Let's formalize it with two cases:
# Case 1: someone claims Enlace needs 6 PB of storage.
records = 6e9 # 6 billion (lesson 4)
real = records * 1_000 # x 1 KB
claim_pb = 6e15 # 6 PB claimed
print(f"real storage = {real/1e12:.0f} TB = {real/1e15:.3f} PB")
print(f"claim (6 PB) is {claim_pb/real:.0f}x inflated -> IMPOSSIBLE")
# Case 2: someone claims Enlace does 4 million reads/s.
claim_qps = 4_000_000
servers = claim_qps / 2000 # at ~2000 req/s per server
print(f"4M reads/s would ask for ~{servers:.0f} servers (real: ~6)")
print(f"the claim is {claim_qps/4000:.0f}x inflated -> IMPOSSIBLE for this scale")
What to expect.
real storage = 6 TB = 0.006 PB
claim (6 PB) is 1000x inflated -> IMPOSSIBLE
4M reads/s would ask for ~2000 servers (real: ~6)
the claim is 1000x inflated -> IMPOSSIBLE for this scale
Both cases are factor-1,000 errors, the classic signature of an extra zero or a mistranslated unit (the Spanish "billón" against the English "billion" from lesson 2, or confusing TB with PB). The smell test catches them by asking "does this mark make sense for what it represents?": Enlace storing 6 PB would imply each URL weighs a megabyte (absurd for text), and Enlace with 4M reads/s would imply 2,000 servers for a URL shortener (absurd for its scale). Neither passes the sniff. The technique: when a number surprises you, don't accept it or discard it blindly —translate it into something tangible (bytes per URL, servers needed) and ask whether that something is credible—. Gross errors almost always give themselves away when translated.
Technique 4: reasoning by orders of magnitude
The fourth technique is the attitude that wraps the other three: judge the mark, not the digit. As lesson 1 hammered, what decides the design is the power of 10, not the decimals. A sanity check doesn't seek to confirm the number is 40 and not 38.58; it seeks to confirm it's "tens" and not "thousands" or "units". That's why the checks are done on marks: two paths that fall on the same mark (even if they differ 4%) confirm; a reference on the same mark validates; a number three marks away from where it should be is impossible. Always asking "which mark did it fall on and is it the one I expected?" is the cheapest sanity check and the one that catches the most errors, because the errors that matter are the mark ones.
Worked example: verify all of Enlace's table at a glance
Let's put the techniques together in a quick check of Enlace's four numbers, as you'd do before delivering them. For each one: the expected mark and a reference that validates it.
checks = [
# number, value, mark, reference that validates it
("Write QPS", "~40/s", "10^1", "a modest DB does thousands/s -> trivial"),
("Read QPS", "~4,000/s", "10^3", "thousands/s: loadable, calls for cache"),
("Storage", "~6 TB", "10^12 B", "6e9 records x 1KB; ~a few disks"),
("Bandwidth", "~2 MB/s", "10^6 B/s","<5% of a 1 Gbps NIC (125 MB/s)"),
("Memory (WS)", "~333 MB", "10^8 B", "0.006% of 6TB; 2% of 16GB RAM"),
]
print(f"{'number':<16}{'value':<10}{'mark':<9}reference")
for n, v, m, r in checks:
print(f"{n:<16}{v:<10}{m:<9}{r}")
What to expect.
number value mark reference
Write QPS ~40/s 10^1 a modest DB does thousands/s -> trivial
Read QPS ~4,000/s 10^3 thousands/s: loadable, calls for cache
Storage ~6 TB 10^12 B 6e9 records x 1KB; ~a few disks
Bandwidth ~2 MB/s 10^6 B/s <5% of a 1 Gbps NIC (125 MB/s)
Memory (WS) ~333 MB 10^8 B 0.006% of 6TB; 2% of 16GB RAM
Five numbers, each on a different mark and with a reference that backs it. None clashes with a known capacity, none falls in an absurd mark, and the relationships between them are coherent (the read is 100× the write; the working set is a tiny fraction of the storage). This mental table —number, mark, reference— is the final sanity check before delivering, and it's exactly the structure you'll fill in lesson 8. If any row didn't have a reference that validates it, or fell outside its expected mark, there would be the error to investigate.
Common mistakes
Reporting false precision (dragging decimals you don't know). What happens: someone delivers "38.58 writes/s" or "6.144 TB" and feels rigorous for the decimals, when the input ("~100M/month") only justifies one figure. Why it happens: in school more decimals meant a higher grade; in estimation it's the reverse, because the decimals fake a precision that doesn't exist. How to detect it: if your result has more significant figures than your most imprecise input, you invented precision. How to fix it: round to one significant figure (~40/s, ~6 TB). A rounded number communicates honestly how much you know; a precise decimal lies. Rule: the result can't be more precise than the most imprecise of its inputs, and in napkin math they're all one figure.
Trusting a single path (not crossing the calculation). What happens: the person computes a number once, types it, and takes it as good without verifying —and doesn't notice they put an extra zero or divided by the wrong number—. Why it happens: the calculation "looks fine" and verifying seems like extra work. How to detect it: if you can't reach a number by a second route, you haven't verified it, you've only computed it. How to fix it: for each important number, find a second path (go down the time staircase instead of dividing at once; start from the total instead of the rate) and check that they fall on the same mark. If they diverge more than your rounding explains, there's a bug. Two paths that agree is the cheapest and most powerful verification there is.
Accepting a number without translating it into something tangible (skipping the smell test). What happens: someone sees "Enlace needs 6 PB" or "does 4M reads/s" and notes it without feeling the alarm, because the number "sounds like a big system". Why it happens: enormous figures impress and are accepted out of inertia, especially under pressure. How to detect it: if you accepted a number only because "it sounds important" without asking yourself what it implies, you skipped the instinct. How to fix it: translate every surprising number into something tangible —bytes per element, servers needed, comparison with a reference— and ask whether that something is credible. 6 PB implies 1 MB per URL (absurd for text); 4M reads/s implies 2,000 servers (absurd for a shortener). Factor-1,000 errors —an extra zero, a confused unit— always give themselves away when translated.
Exercises
Exercise 1 — Round and place the mark. For each raw calculation, (i) round to one significant figure, (ii) write it as digit × power of 10, and (iii) say in one word which "mark" it falls on: (a) 38.58 writes/s; (b) 27,000,000,000,000 bytes (Enlace's provisioned storage); (c) 172,800,000,000 bytes/day (the daily egress).
See solution
- (a)
38.58/s→ ~40/s =4 × 10¹→ mark: tens per second (trivial for a DB). - (b)
27,000,000,000,000 B→ ~30 TB (one figure) or, keeping a bit more for the table, ~27 TB =2.7 × 10¹³ B→ mark: tens of terabytes (a handful of servers). Note: at strict one significant figure,27rounds to30; in the table we usually keep27because27 = 9 × 3comes from exact factors (9 TB × 3 replicas), not from an imprecise measurement. Both say "tens of TB". - (c)
172,800,000,000 B/day→ ~200 GB/day =2 × 10¹¹ B→ mark: hundreds of gigabytes a day (modest egress, cheap in the cloud).
In all three cases the value of reporting is the mark: tens/s, tens of TB, hundreds of GB/day. The exact digit almost never changes the decision; the mark always.
Exercise 2 — Cross two paths. Verify Enlace's read bandwidth (~2 MB/s) by two independent paths: (a) directly, as QPS × payload; (b) starting from the daily egress (~173 GB/day from lesson 5) and going down to "per second". Do they fall on the same mark? If they differ, does the rounding explain the difference?
See solution
- (a) Direct:
4,000 reads/s × 500 B = 2,000,000 B/s = 2 MB/s. - (b) From the daily egress:
173 GB/day ÷ 86,400 s/day = 173 × 10⁹ / 86,400 ≈ 2.0 × 10⁶ B/s = 2 MB/s.
Both fall on the same mark (~2 MB/s, 10⁶ B/s) and in fact match almost exactly, because the daily egress came from that same bandwidth multiplied by the seconds of the day —dividing by 86,400 undoes it—. When a second path is the "inverse" of the first (multiply by time and then divide by time), the check confirms there wasn't a typing error in the round trip. The mark matches, the rounding introduces no notable difference, and the number is verified. The lesson: even partially dependent paths catch typing and unit errors, which are the most common.
Exercise 3 — The smell test. A colleague presents these three figures for Enlace. Without recalculating in depth, use your instinct and a reference to say which is impossible and why, translating it into something tangible: (a) "the working set is ~330 GB"; (b) "the peak write QPS is ~120/s"; (c) "the storage at 5 years is ~600 GB".
See solution
- (a) Working set ~330 GB: IMPOSSIBLE (factor ~1,000 inflated). The real working set is ~333 MB, not GB. Translation: 330 GB would imply caching ~660 million 500 B URLs (
330e9 / 500 = 6.6×10⁸), which is 200 times the ~3.3M distinct URLs Enlace has in play in a day. The correct mark is hundreds of MB (10⁸ B), not hundreds of GB (10¹¹ B). Alarm: they almost certainly confused MB with GB. - (b) Peak write QPS ~120/s: correct. Reference: the average is ~40/s (100M/month), the peak factor is 3x,
40 × 3 = 120/s. Mark of tens-hundreds per second, trivial for a DB. It passes the sniff. - (c) Storage ~600 GB: suspicious (factor ~10 low). The real one is ~6 TB = 6,000 GB, not 600. Translation: 600 GB would imply
600e9 / 1000 = 6×10⁸records, that is 600 million, when Enlace accumulates 6 billion in 5 years (10× more). Correct mark 10¹² B (TB), not 10¹¹ B. Alarm: they probably counted 6 months instead of 5 years, or dropped a zero.
The technique in all three: translate the number into something countable (cached URLs, records, servers) and compare it with a reference you already have. Gross errors —factors of 10 or 1,000— give themselves away when translated; the correct number (b) survives the translation without creaking.
Summary and next step
In this lesson you learned the discipline that makes an estimate trustworthy. The smart rounding: reporting one significant figure (because your inputs have one), rounding the inputs from the start and the results at the end, and thinking in powers of 10 so that mark errors are visible. And the four sanity checks: crossing two independent paths (and knowing whether their difference is explained by rounding or a bug), comparing against known references (the memory ≪ SSD ≪ disk ≪ network latencies, the typical sizes), applying the smell test by translating every surprising number into something tangible, and —the attitude that wraps them— judging the mark, not the digit. You verified Enlace's complete table at a glance: each number on its mark, each with a reference that backs it.
Before moving on you should be able to: round any raw result to one significant figure and place its mark; verify a number by a second path and decide whether the difference is acceptable; cite from memory the latency hierarchy (memory/SSD/disk/network) and a handful of reference sizes; and detect an impossible number by translating it into something countable.
You now have everything: the four numbers (lessons 3–6) and the discipline to trust them (this lesson). Lesson 8 is the module's capstone: starting only from Enlace's scale, you'll produce from scratch the complete capacity table —read and write QPS, average and peak; storage with indexes and replicas; bandwidth; working-set memory— with each calculation run, a sanity check per row, and a note of assumptions. It's where the whole module comes together in a deliverable you can defend in front of whoever asks.
Resources
- Jeff Dean, "Latency Numbers Every Programmer Should Know" — living compilation at gist.github.com/jboner/2841832, and Colin Scott's interactive-by-year version at colin-scott.github.io/personal_website/research/interactive_latency.html. The reference latency table (memory vs SSD vs disk vs network) we use for the sanity checks. Orders of magnitude, not exact values; they change with the hardware and the year. In English.
- The System Design Primer, "Back-of-the-envelope calculations" and "Powers of two / Latency numbers" section — github.com/donnemartin/system-design-primer#appendix. It gathers the size and latency references and shows how they're used to validate estimates. Free, in English.
- Sanjoy Mahajan, Street-Fighting Mathematics (MIT Press, freely available) — mitpress.mit.edu/books/street-fighting-mathematics. A whole book about the discipline of estimating by orders of magnitude, rounding with judgment, and verifying with quick checks —the same mindset as this lesson, taken in depth—. Free in PDF; in English.