Module 2: Napkin Estimation (Back-of-the-Envelope)
3. QPS: from requests per month to requests per second
Description
The first of the four capacity numbers, and the one you'll use most in your entire systems-engineering life, is the QPS: queries per second. It's the system's pulse: how many things it has to serve each second. Almost everything else is sized from it —how many servers, how much database, whether a cache is needed— because the QPS is the instantaneous load the system must sustain without drowning.
In this lesson you convert Enlace's anchor number, "100 million new URLs a month", into QPS. You'll produce three figures: the write QPS (the URLs that are created, ~40/s), the read QPS (the ones that are visited, ~4,000/s, via the 100:1 ratio), and —this lesson's new thing— the peak QPS, which is higher than the average because real traffic isn't even, and it's the one you really have to handle. By the end, you'll know why Enlace's write worries no one, why the read does, and why a system is sized for its worst minute and not its average minute.
Connection to the module: this lesson builds the first row of the capacity table —the QPS— using lesson 2's kit (powers of 10, seconds per month). The QPS numbers you produce here feed the following lessons directly: bandwidth (lesson 5) is QPS × payload, and working-set memory (lesson 6) starts from reads per day. The average-vs-peak distinction you introduce here is used in all four estimates. The decision of what to do with 4,000 reads/s —add a cache, add replicas— belongs to modules 4 and 5; here we only produce the number that justifies it.
The tollbooth
Imagine a tollbooth on a highway. The capacity question is: how many cars per second does it have to serve? If they're told "3 million cars pass a month", the operator can't plan anything useful with that number: they need to translate it to cars per second, because the booths, the lanes, and the staff are sized by the instantaneous rate, not by the monthly total. Divide 3 million by the ~2.6 million seconds of a month and it gives ~1.2 cars per second. With that they already know how many lanes to open at a normal moment.
But here's the trap every toll operator knows and every novice engineer forgets: the traffic isn't even. At 3 a.m. a car passes every several minutes; at 6 p.m., rush hour, ten times more than the average pass. If the operator sizes the booth for 1.2 cars/s (the average), at 6 p.m. a line kilometers long forms, because at that moment 12 cars/s arrive and the lanes open for 1.2 can't keep up. The booth has to be sized for the peak hour, not the day's average, or it collapses exactly when it's used most.
A system's QPS is exactly this. "100 million URLs a month" is the total; the average QPS is that total spread evenly across all the seconds; and the peak QPS is what arrives at the moment of most traffic, which is several times the average. The system is sized for the peak, because a system that only handles the average goes down at its peak hour —which is, by definition, when the most people are using it and the worst moment to go down—.
What exactly the QPS is
QPS (queries per second) is the number of requests a system receives per second. "Query" here doesn't only mean database queries: it means any request the system serves —an API call, a redirect, a write—. You'll also see RPS (requests per second), which is the same with a different name; in this guide we use QPS.
The crucial thing is that the QPS breaks down by operation type, because each operation costs differently. In Enlace there are two operations and their costs are wildly different:
shorten(write): takes a long URL, generates ashort_code, and stores it. It's a write to the database. It costs.resolve(read): takes ashort_code, looks up thelong_url, and redirects. It's a read. It costs much less, and —as we'll see— it can be cached almost entirely.
That's why you never estimate "Enlace's QPS" as a single number: you estimate the write QPS and the read QPS separately. A system can have a trivial write QPS and a brutal read one (like Enlace), and mixing them into a single number would hide exactly the asymmetry that defines the design. The rule: separate reads from writes from the first number.
Worked example 1: the write QPS
The input figure is "100 million new URLs per month". Each new URL is a shorten operation, that is, a write. The question: how many writes per second on average?
The calculation is the total over the seconds of a month (which you nailed down in lesson 2: 30 × 24 × 3,600 = 2,592,000 s):
qps_write = 100,000,000 / 2,592,000 ≈ 38.6 → ~40 writes/s
Let's run it:
# Enlace's write QPS: 100M URLs/month -> writes/s
writes_per_month = 100_000_000
seconds_per_month = 30 * 24 * 3600
qps_write = writes_per_month / seconds_per_month
print(f"seconds_per_month = {seconds_per_month:,}")
print(f"qps_write (raw) = {qps_write:.2f}/s")
print(f"qps_write (rounded) = ~{round(qps_write / 10) * 10}/s")
What to expect.
seconds_per_month = 2,592,000
qps_write (raw) = 38.58/s
qps_write (rounded) = ~40/s
~40 writes per second. And with that number you already have an architecture conclusion, without drawing anything: 40 writes/s is a trivial load. To calibrate, a single modest relational database (a PostgreSQL on a decent server) handles on the order of thousands of simple inserts per second without breaking a sweat. 40/s is two orders of magnitude below that. So Enlace's write doesn't need anything special: no sharding, no queues, no tricks. A single database is plenty. The write isn't Enlace's problem, and knowing it from the first number saves you designing solutions for a problem that doesn't exist.
Worked example 2: the read QPS, chained
The second number doesn't need new data from the prompt: it comes from the first plus the ratio. The anchor number says read:write = 100:1, that is, for each URL created, it's visited 100 times on average (people share a link and many open it). So the read QPS is the write one times 100:
qps_read = qps_write × 100 = 40 × 100 = 4,000 reads/s
qps_write = 38.58 # raw, to not drag the rounding yet
read_write_ratio = 100
qps_read = qps_write * read_write_ratio
print(f"qps_read (raw) = {qps_read:.0f}/s")
print(f"qps_read (rounded) = ~{round(qps_read, -3):.0f}/s")
What to expect.
qps_read (raw) = 3858/s
qps_read (rounded) = ~4000/s
~4,000 reads per second. And here Enlace's character reveals itself entirely. The read is 100 times the write: two orders of magnitude of difference. Enlace is a read-heavy system, and that's its defining property. While 40 writes/s any database does, 4,000 reads/s is already a load to take seriously: although a database can serve 4,000 reads/s, doing it by hitting the disk on each one is fragile and expensive, and worse if the traffic grows. This is the reason the cache (module 4) and read replicas (module 5) exist: to protect that 4,000/s read path. The number you just computed is the one that justifies half of Enlace's architecture.
Notice the pattern of chaining: the read QPS didn't come from dividing the prompt again, but from multiplying a number you already had (the write one) by a factor from the prompt (the ratio). The module's four numbers chain like this; learning to see which number feeds which is half the skill.
Where does the 100:1 ratio come from?
In Enlace the 100:1 ratio is given by the prompt, but it's worth understanding why it's believable and what you'd do if you weren't given it, because in a real problem you almost never get the ratio for free. The intuition is from the domain itself: a URL shortener exists to share links, and a link is created once but opened many times. Someone shortens the link to an article and posts it on a social network; that single write generates dozens or hundreds of reads as people click. A ratio of 100 reads per write is, if anything, conservative for a popular link —the viral ones reach thousands—, and very generous for one nobody opens. On average across the whole system, 100:1 is a reasonable and round figure, and that's why it's the anchor number.
If a prompt didn't give you the ratio, you'd estimate it by reasoning about the typical behavior: how many times is each thing that's written read? For a social feed it could be 10:1 or 100:1 (a post is written, many followers read it); for an audit-log system it could be 1:100 the other way (much is written, rarely read); for a chat, close to 1:1 (each message sent is delivered and read once or twice, as in exercise 1). The ratio is an assumption you state, just like the peak factor. What matters isn't getting it right to the decimal, but placing it on the correct mark —is this system read-heavy, write-heavy, or balanced?— because from that mark comes half the design. For Enlace the answer is crystal clear: read-heavy, 100:1, and everything else follows from there.
The new thing: average vs. peak
Everything above was the average QPS: we spread the month's total evenly across all the seconds, as if at 3 a.m. Enlace received as many visits as at noon. It's not so. The real traffic of almost any human-facing service has a daily curve: it rises during active hours, drops in the early morning, and has peaks. Sketched roughly, a day of Enlace looks more or less like this:
Read QPS over a day (schematic)
██
peak ~12,000 ┤ ████
┤ ██████ ██
┤ ██ ██████ ██ ████
avg ~4,000 ┤········██···██···██████···██···████···██······· ← average
┤ ██ ████ ████ ██████ ████ ██████ ████ ██
valley ~1,000 ┤ ████████████████████████████████████████████
└──┬────┬────┬────┬────┬────┬────┬────┬────┬──
0 3 6 9 12 15 18 21 24 hour
The average (~4,000/s) is the dotted line, but the system doesn't live in the average: it lives both in the early-morning valley (~1,000/s) and in the afternoon peak (~12,000/s). And here's the golden rule of sizing:
A system is sized for the peak, not the average. A system that only handles the average collapses at its peak hour, which is exactly when the most people use it.
How much higher is the peak than the average? That ratio is called the peak factor (peak-to-average ratio), and for human-facing web services it's usually between 2x and 3x. It's not a physical law; it depends on the service (one used during business hours has sharper peaks than a 24/7 global one), but 2-3x is the reasonable napkin range. Applying it to Enlace:
qps_write_avg = 40
qps_read_avg = 4000
for factor in (2, 3):
print(f"peak x{factor}: write {qps_write_avg*factor}/s read {qps_read_avg*factor}/s")
What to expect.
peak x2: write 80/s read 8000/s
peak x3: write 120/s read 12000/s
So Enlace, at its peak hour, receives on the order of ~80–120 writes/s and ~8,000–12,000 reads/s. These are the numbers to size for. Note that even the write peak (120/s) is still trivial for a database —Enlace's write doesn't worry anyone even at the peak—, while the read peak (12,000/s) confirms even more strongly the need for cache and replicas: twelve thousand reads per second hitting a single database's disk is a recipe for disaster.
A nuance the daily curve makes visible: since the valley (~1,000/s in the early morning) is a fraction of the peak (~12,000/s), having servers on for the peak 24 hours wastes capacity at night. That's why modern systems scale with the curve —they turn on machines for the peak and turn them off in the valley, which is called auto-scaling—, paying only for what they use at each moment. The estimation of the peak and the valley is exactly what feeds that policy: without both numbers you wouldn't know between how many and how many machines to oscillate. We don't design auto-scaling in this guide (it's a deployment and operations topic), but the average-peak pair you compute here is its direct input, and it's another reason to always report both.
How to report it. A mature estimator gives both figures: "~4,000 reads/s on average, ~8,000–12,000/s at peak (2–3x factor)". Reporting only the average hides the real requirement; reporting only the peak exaggerates the typical load. The two together tell the truth: the system almost always operates at the average but must survive the peak.
With this, the QPS row of Enlace's capacity table is complete. This is the way you'll deliver it in lesson 8:
| Operation | Average QPS | Peak QPS (3x) | Conclusion |
|---|---|---|---|
Write (shorten) | ~40/s | ~120/s | trivial even at peak; a single DB is plenty |
Read (resolve) | ~4,000/s | ~12,000/s | calls for cache + replicas; ~half a dozen servers |
Four numbers in a row, each defensible, and a design conclusion per operation. That density —number + what it means— is what distinguishes a useful capacity table from a list of loose figures.
The same number's staircase: month → day → second
A useful trick to not get lost and to be able to verify is computing the QPS in steps, going down the staircase of time units, instead of dividing at once by 2.6 million. Sometimes a prompt gives you "per day" and other times "per month", and going down the staircase works the same. For Enlace's reads:
reads_per_month = 100_000_000 * 100 # 100M writes x 100 reads each
reads_per_day = reads_per_month / 30
reads_per_hour = reads_per_day / 24
reads_per_sec = reads_per_hour / 3600
print(f"reads/month = {reads_per_month:,.0f}")
print(f"reads/day = {reads_per_day:,.0f}")
print(f"reads/hour = {reads_per_hour:,.0f}")
print(f"reads/sec = {reads_per_sec:,.0f} -> ~4,000/s")
What to expect.
reads/month = 10,000,000,000
reads/day = 333,333,333
reads/hour = 13,888,889
reads/sec = 3,858 -> ~4,000/s
Each step is a division by the corresponding time conversion (÷30 to go from month to day, ÷24 from day to hour, ÷3,600 from hour to second), and at the end you land on the same ~3,858/s you got by dividing at once by 2.6 million. Going down the staircase is useful for two reasons: first, it's easier to verify in your head (dividing by 30, by 24, and by 3,600 separately is more manageable than by 2,592,000); second, it hands you valuable intermediate numbers. In particular, the "per day" step gives you ~333 million reads per day (or ~345.6 million if you start from the rounded 4,000/s × 86,400). That "per day" isn't a waste: it's exactly the input for lesson 6 to estimate the cache's working set. The module's numbers are recycled between lessons; computing the read-per-day here gets work done ahead for over there.
From QPS to servers: what the number is for
The QPS isn't decoration; its purpose is to answer "how many machines do I need?". The calculation is as simple as the rest of the module: if you know (or assume) how many requests one server handles per second, the number of servers is the peak QPS divided by that capacity, plus a margin.
Suppose an Enlace application server, serving redirects from cache, comfortably handles about 2,000 requests/s (a reasonable napkin assumption for light work). Then:
qps_read_peak = 12_000 # read peak (3x factor over 4,000)
per_server = 2_000 # assumed capacity per server
servers_needed = qps_read_peak / per_server
print(f"servers for the peak = {servers_needed:.0f}")
print(f"with margin (+1 spare) = {servers_needed + 1:.0f}")
What to expect.
servers for the peak = 6
with margin (+1 spare) = 7
On the order of half a dozen application servers cover Enlace's read peak, plus one spare in case any fails. That "+1" isn't a whim: it's the redundancy principle (module 7) peeking out —if you size with exactly the ones you need and one goes down, the rest are overloaded—. Notice the complete chain of reasoning you just walked: prompt (100M/month) → average QPS (4,000/s) → peak QPS (12,000/s) → servers (≈7). Each arrow was a napkin calculation, and at the end you have a number of machines you can defend. That's what the QPS is for: not to know it, but to size with it.
The same calculation, done with the write peak, confirms what you already suspected: 120/s ÷ 2,000/s per server ≈ 0.06 servers, that is, a fraction of a single server. Enlace's write doesn't justify even one dedicated machine; it fits easily on any of them. All the capacity pressure is on the read, and the QPS says it with numbers.
A bit of intuition: the QPS of other systems
Numbers in the abstract don't say much until you have something to compare them with. This table (approximate, order-of-magnitude values, to calibrate your ear) places Enlace's read QPS among systems of different scales:
| System | Read QPS (order of magnitude) | What load it is |
|---|---|---|
| A personal blog | ~1–10/s | trivial: one server is plenty |
| Enlace (our case) | ~4,000/s (peak ~12,000/s) | medium: calls for cache and a few machines |
| A large enterprise API | ~10⁵/s (hundreds of thousands) | high: replicas, sharding, many servers |
| A global giant (search engine, top social network) | ~10⁶–10⁷/s (millions) | extreme: thousands of machines, multiple data centers |
Enlace lives in the middle band: it's not a toy (4,000/s already forces thinking about a cache), but it's far from needing a giant's machinery. This placement matters for the design: it tells you Enlace's solutions will be "a cache, some replicas, half a dozen servers", not "global sharding with hundreds of nodes". Designing Enlace as if it were a giant would be the over-engineering lesson 1 vaccinated you against; the QPS, compared to these references, keeps you grounded.
QPS isn't the same as simultaneous connections
It's worth separating two numbers that are often confused, because they measure different things and size different resources. The QPS counts requests that start and finish each second. The simultaneous connections (or concurrency) count how many requests are open at once at a given instant. They aren't the same, and the bridge between them is how long each request lasts.
The relationship, known as Little's Law in its simple form, is:
simultaneous connections ≈ QPS × average duration of each request
For Enlace, a redirect from cache is very fast —say each resolve takes about 10 ms (0.01 s) from start to finish—. Then, at the peak:
qps_read_peak = 12_000
seconds_per_request = 0.010 # 10 ms per redirect from cache
concurrent = qps_read_peak * seconds_per_request
print(f"simultaneous connections ~ {concurrent:.0f}")
What to expect.
simultaneous connections ~ 120
Although Enlace serves 12,000 requests per second at the peak, at any given instant there are only about ~120 open at once, because each one lives barely 10 ms. This distinction is what sizes things like the number of database connections or the size of the pools: you don't need 12,000 open connections, you need ~120. And it reveals something important: if the requests were slow, the concurrency would explode. If each resolve took 1 second instead of 10 ms (for example, hitting the disk without a cache), the concurrency would jump to 12,000 simultaneous connections —a hundred times more—, and there the system really would drown. Another reason, said with numbers, why Enlace's cache matters: it not only lowers the disk load, but keeps the requests short and therefore the concurrency low.
Common mistakes
Sizing for the average and not for the peak (undersizing). What happens: someone computes "4,000 reads/s" and sizes the system exactly for that, and the system goes down every afternoon at peak hour. Why it happens: the average is the number that comes directly from the division, and it's easy to forget that real traffic isn't even. How to detect it: if your design handles exactly the average QPS and not a bit more, it has no margin for the peak. How to fix it: multiply the average by the peak factor (2–3x) and size for that number. And report both. The average is for understanding the volume; the peak is for sizing the capacity.
Mixing reads and writes into a single QPS (aggregation that hides). What happens: the person reports "Enlace does ~4,040 requests/s" by adding reads and writes, and with that loses the most important information about the system. Why it happens: it gives the impression that "the total QPS" is simpler. How to detect it: if your QPS number doesn't distinguish read from write, you hid the 100:1 asymmetry that defines Enlace. How to fix it: always report both separately. The gap between them (40 vs 4,000) is what dictates half the design; adding them erases it.
Confusing "users" with "requests" (wrong unit). What happens: someone reads "Enlace has 10 million users" and plugs it in as if it were QPS or requests/month. Why it happens: prompts mix business metrics (users, accounts) with load metrics (requests), and not all of them serve to estimate QPS. How to detect it: if your QPS came from a "users" number without passing through "how many requests each user makes", you skipped a step. How to fix it: to get to QPS you need events per unit of time (URLs/month, visits/day), not a count of people. If you're only given users, you have to assume how many actions each one makes —and state that assumption—. In Enlace the prompt already gives you the event directly (100M URLs/month), so you don't fall into this; but in other problems it's the first trap.
Exercises
Exercise 1 — The QPS of a new service. A messaging service processes 2 billion messages sent per day. (a) What's the average write QPS? (b) If each message is read (delivered and opened) about 2 times on average, what's the read QPS? (c) With a peak factor of 2x, what's the peak read QPS? Show the arithmetic with powers of 10.
See solution
- (a) 2 billion/day = 2 × 10⁹ / day. A day = 86,400 s ≈ 8.64 × 10⁴.
qps_write = 2 × 10⁹ / (8.64 × 10⁴) = (2 / 8.64) × 10⁵ ≈ 0.23 × 10⁵ ≈ 23,000/s. Rounded: ~23,000 writes/s (or "on the order of tens of thousands/s"). - (b) 2:1 ratio, so
qps_read = 23,000 × 2 = ~46,000 reads/s. - (c) Peak 2x:
46,000 × 2 = ~92,000/s, or "~90,000 reads/s at peak".
Note how different it is from Enlace: here the write is already enormous (23,000/s, against Enlace's 40/s) because the input volume is gigantic (2 billion/day against 100 million/month ≈ 3.3 million/day). The method is identical; the numbers change with the scale of the prompt, which is exactly why it's computed and not quoted.
Exercise 2 — Why the peak, not the average? A colleague says: "I sized Enlace for 4,000 reads/s, which is the average; if it sometimes reaches 12,000 in the afternoon, well, let it queue up a bit, it drains on its own when the traffic drops". Explain in two or three sentences why this reasoning is dangerous and what it's missing.
See solution
The reasoning fails because the peak hour isn't an instant, it's hours, and during all that time the load (12,000/s) triples the capacity (4,000/s). A queue that grows for hours at triple the speed it empties doesn't "drain on its own": it explodes —the latency shoots up, requests time out, users see errors—, and all this happens in the window of highest traffic, which is when it matters most. Queuing serves to absorb a brief peak (seconds), not a sustained regime of overload. What it's missing: sizing for the peak (multiplying the average by the 2–3x factor) so the capacity exceeds the load even at the worst moment. The margin isn't a luxury; it's what prevents the collapse at the moment of highest use.
Exercise 3 — Chaining with no new data. Just from Enlace doing ~40 writes/s and the ratio being 100:1, with no other data, derive: (a) the reads per second; (b) the reads per day; (c) the writes per day. Say which of these three numbers you'll need in lesson 6 (working-set memory) and why.
See solution
- (a)
40/s × 100 = 4,000 reads/s. - (b)
4,000/s × 86,400 s/day = 345,600,000 ≈ 345.6 million reads/day. - (c)
40/s × 86,400 = 3,456,000 ≈ 3.46 million writes/day(equivalent to the ~3.33 million that come from dividing 100M/month by 30; the small difference comes from rounding 38.6 to 40).
The key number for lesson 6 is the writes per day (~3.3 million), because they're the new URLs that come in each day, and the cache's working set is estimated as a fraction (the hot 20%) of the distinct URLs in play —and the day's new URLs are a good proxy for that changing set—. The reads per day (345.6M) count visits, not distinct URLs: a single viral URL is read thousands of times but takes up a single entry in the cache. Distinguishing "events" from "distinct things" is the key to memory estimation, and we develop it in lesson 6.
Summary and next step
In this lesson you produced the first row of Enlace's capacity table: the QPS. You computed the write QPS (100M/month ÷ 2.6M s ≈ 38.6 → ~40/s), chained it with the 100:1 ratio to get the read QPS (40 × 100 = ~4,000/s), and applied the peak factor (2–3x) for the numbers you really have to handle: ~80–120 writes/s and ~8,000–12,000 reads/s at peak hour. You learned the golden rule —you size for the peak, not the average— and why you separate reads from writes from the first number: the 100x gap between them is Enlace's read-heavy signature and the justification for the cache and replicas to come.
Before moving on you should be able to: convert "N events per month (or per day)" into QPS by counting zeros; chain the read QPS from the write one via the ratio; apply a peak factor and explain why you size for the peak; and report average and peak together, separating read from write.
The next number is storage. In lesson 4 you'll answer "how much disk does Enlace need in five years?" by multiplying three things —how many records per month, for how many months, and how much each record weighs— to reach the ~6 TB. It's a different kind of calculation from the QPS (it accumulates over time instead of spreading per second), and it brings its own subtleties: the indexes, the replicas, and what "fits but is planned" means.
Resources
- Martin Kleppmann, Designing Data-Intensive Applications, Chapter 1, "Describing Load" and the Twitter example — dataintensive.net. Kleppmann uses Twitter's fan-out to show how the read/write ratio decides the whole architecture, exactly the reasoning we applied to Enlace's 100:1. Highly recommended reading; in English.
- The System Design Primer, "Back-of-the-envelope" and the URL shortener example (Pastebin/URL shortener) — github.com/donnemartin/system-design-primer. It walks through a QPS calculation for a case almost identical to Enlace. Free, in English.
- PostgreSQL documentation, "Performance Tips" — postgresql.org/docs/current/performance-tips.html. To calibrate what "40/s is trivial" and "12,000/s must be taken seriously" mean in a real database. You don't need to read it whole; it serves to put the QPS numbers in context.