Capacity estimation — design starts from the numbers
Asked to "design a service used by 100,000 people daily", the first thing to do is not draw an architecture diagram. Produce numbers.
How many requests per second? How many gigabytes per year? Once these are out, arguments end. Whether caching is needed and whether to split the database are decided by numbers. Lining up architecture without numbers gives a design with unneeded parts.
What you'll learn in this chapter
- Deriving requests per second from user count
- Placing peak multipliers (how many times average)
- The read/write ratio splitting the setup
- Getting data volume and bandwidth from record size
- The precision idea "as long as orders of magnitude line up"
- Conditions under which "one machine is enough" can be said
Builds on "think in orders of magnitude" from Complexity. That one counted step orders; this one counts load and data orders.
What this chapter does not cover
How to handle the estimated load is covered in later chapters. Adding capacity in Scaling directions; dealing with failure in Availability and redundancy; splitting data in Data distribution.
Concrete cloud instance selection and pricing is in Practical AWS Guide.
Why start from numbers
"Should we add a cache?" isn't answerable alone. At 5 requests per second, no. At 5,000, yes. The answer flips depending on the numbers.
Long design arguments usually happen because everyone is imagining a different scale. Placing numbers first eliminates that mismatch.
The goal of estimation isn't accuracy. It's narrowing the choices of setup.
Deriving requests per second
The starting point is user count. Three stages descend from there.
| Stage | What to compute | Formula |
|---|---|---|
| 1 | Requests per day | Daily users × actions per user |
| 2 | Average requests per second | Requests per day ÷ 86,400 |
| 3 | Peak requests per second | Average × peak multiplier |
A day is 86,400 seconds. Memorizing that speeds up calculation.
const SECONDS_PER_DAY = 86_400;
type Workload = {
dailyUsers: number; // Daily users
actionsPerUser: number; // Requests one user makes in a day
peakFactor: number; // Peak as multiple of average
};
function requestsPerSecond(w: Workload) {
const perDay = w.dailyUsers * w.actionsPerUser;
const average = perDay / SECONDS_PER_DAY;
return { perDay, average, peak: average * w.peakFactor };
}
const r = requestsPerSecond({ dailyUsers: 100_000, actionsPerUser: 20, peakFactor: 10 });
console.log(`${r.perDay.toLocaleString()} requests/day`);
console.log(`average ${r.average.toFixed(1)} rps / peak ${r.peak.toFixed(0)} rps`);
2,000,000 requests/day
average 23.1 rps / peak 231 rps
Against the impression of "100,000 daily users", the average is 23 rps. At this scale, even a single app server can handle it. What changes the judgment is the peak.
Peak as multiple of average
Usage doesn't spread evenly across 24 hours. Consumer services skew to evening; business systems to weekday daytime.
Measure the multiplier if you can. For existing services, hourly access log breakdowns work directly. If you can't measure something new, place it with the assumption stated.
| Situation | Placement |
|---|---|
| Widely spread across a day (business systems) | About 3× average |
| Concentrated at specific times (consumer) | 5–10× |
| Fixed start time (ticket sales, lottery) | Average is meaningless. Estimate a per-second ceiling separately |
These multipliers have no citation. Use as starting values, and swap in measurements when you can. The value smoothed over an hour differs from the peak-within-a-minute, so also record the smoothing granularity.
Row 3 matters. For load where everyone shows up at sale start, multiplying from daily average doesn't reach the real value. Place "how many people arrive in the same one second" directly.
Once placed, write in the design doc that it's an assumption. So when actuals differ later, you can trace which premise fell.
Read/write ratio
Even at the same requests per second, reads and writes are handled differently.
- Reads can be scattered. They can go on caches; they can be split across replicated databases
- Writes cannot be scattered. They must be collected at one place in the correct order
So produce not just the total but the ratio. Read-centric services are 100:1 or more read-skewed.
If the earlier peak of 231 rps is 100:1, writes are 2.3 requests per second. Once visible, you can say "no need to do anything for writes yet".
How to scatter reads is Scaling directions; what breaks when you split writes is Data distribution.
Data volume and bandwidth
Next, data. Compute as record size × count.
Row growth rate isn't necessarily the write request count. Business data rows grow with writes, but access logs at 1 row per request grow with reads too. Split by kind.
const KB = 1_000;
const GB = 1_000_000_000;
function storagePerYear(rowsPerDay: number, bytesPerRow: number, overhead: number) {
const rawPerYear = rowsPerDay * bytesPerRow * 365;
return { rawGB: rawPerYear / GB, actualGB: (rawPerYear * overhead) / GB };
}
// Business data: 20,000 writes/day (1% of 2M requests), 1 KB per row
const biz = storagePerYear(20_000, 1 * KB, 3);
// Access logs: 1 row per request across all, 0.5 KB per row
const log = storagePerYear(2_000_000, 0.5 * KB, 3);
console.log(`business raw ${biz.rawGB.toFixed(1)} GB/yr → actual ${biz.actualGB.toFixed(0)} GB/yr`);
console.log(`access logs raw ${log.rawGB.toFixed(0)} GB/yr → actual ${log.actualGB.toFixed(0)} GB/yr`);
business raw 7.3 GB/yr → actual 22 GB/yr
access logs raw 365 GB/yr → actual 1095 GB/yr
50× difference. Within the same service, per-kind capacity planning is a separate discussion. Business data fits on one machine; logs grow without an expiry policy.
Don't use raw data volume as the capacity plan. Indexes, replicas, backups, fragmentation inflate it. We multiply by 3 here. This multiplier has no citation and varies by setup, but 2–3× keeps you close. That indexes eat capacity is covered by Indexes.
Bandwidth comes out the same way. If a response is 50 KB, peak 231 rps is 11.6 MB/s, roughly 92 Mbps. For image or video services, this is what caps first.
Orders of magnitude are enough
There's a benchmark for how precise to be. To the granularity that changes the decision.
- 2× error doesn't change the design. 231 rps vs. 400 rps is the same setup
- 10× error changes it. 231 rps vs. 2,300 rps changes the number of machines and whether caching is needed
So don't agonize over "20 or 25 requests per user per day". Spend effort not confusing 20 with 200.
This feeling matches Complexity. O(n) vs. O(n²) is decisive; 2n vs. 3n doesn't change the decision. See orders, not coefficients.
Conditions to say "one machine is enough"
Estimation's product isn't only finding insufficient setups. Discarding unneeded ones has equal value.
If these three all hold, you can start with a single-machine setup.
| What to check | Guideline |
|---|---|
| Peak load | Is there headroom against one machine's capacity? Don't design at 100% utilization |
| Data volume | Does it fit on one disk? Even if it fits, how many years does it last at the growth rate? |
| Acceptable downtime | Can you wait for recovery when it goes down? If not, redundancy is needed regardless of load |
Note row 3. Adding machines isn't only for load. If load is fine on one but you can't stop, you have two. That's an availability issue — see Availability and redundancy.
"It might grow in the future" isn't a reason to distribute now. What you need is a form that can grow horizontally when it does. Making it possible and doing it now are different.
Decision procedure
Asked for an estimate, fill in this order.
- Place users and per-user rate — both can be assumptions. Just state they are
- Compute average rps — daily total ÷ 86,400
- Multiply by the peak multiplier — measure if you can. Don't multiply from average for fixed-start-time loads
- Get the read/write ratio — different moves for reads and writes
- Get data volume — record size × count × 2–3
- Judge whether one machine fits — load, capacity, acceptable downtime — three checks
- Only add setup where it's insufficient — don't add where it's already enough
7 is the conclusion. Estimation is done to decide where NOT to add, not to add.
Common misconceptions
"Estimation is meaningless unless it's accurate." — The purpose isn't accuracy but narrowing choices. If orders align, needed vs. unneeded setup separates. Move on with stated assumptions rather than pursuing accuracy.
"Peak is about 2× average." — Depends on domain, but consumer services can be 5–10×. And the value smoothed over an hour differs from the within-minute peak. Be aware of the smoothing granularity.
"10× users means 10× machines." — Proportional only for the request-processing part. Databases hit limits in capacity and writes first; caches let you not need 10× machines. Each part follows a different curve.
Exercises
Q1. 300,000 daily users, 10 requests each per day, peak 6× average. Roughly what peak rps?
Answer and explanation
About 210 rps.
- Requests/day = 300,000 × 10 = 3,000,000
- Average = 3,000,000 ÷ 86,400 ≈ 34.7 rps
- Peak = 34.7 × 6 ≈ 208 rps
Answering by order of magnitude is enough. "Around 200" gets you through downstream arguments.
But this number alone can't decide the machine count. How many rps one machine handles depends on per-request processing time. 10ms vs. 500ms is 50×. Estimation gives the demand-side order; the supply side is only known by measurement.
Q2. In the estimate above, the read/write ratio is 50:1. What's the write peak and what can you say?
Answer and explanation
Write peak is about 4 rps (208 ÷ 51 ≈ 4).
Two things:
- No need for distribution machinery for writes. 4 writes per second is small load for any ordinary database
- Act on the read side if anything. The remaining 204 rps is reads, so caches and replicas are what pay off
Looking only at the total misses this. "208 rps so we split the database" becomes unnecessary once you see writes are 4 rps.
Q3. You're told "small now, but 100× in 3 years is expected". How do you design now?
Answer and explanation
Design for now's scale now, and keep a form that can grow horizontally when it grows.
Building for 100× now means paying 3 years of operations for unused parts. What justifies doing it now is only decisions that are expensive to change later.
| Do now | Reason |
|---|---|
| Don't put state on servers | Extracting state later is more work than adding servers |
| Decide on a key that could become the partition key | Distribution design depends on how data is held |
| Make load and data volume measurable | Materials to judge whether 100× actually comes |
Conversely, adding machines, adding replicas, and inserting caches all fit in "when needed later".
Externalizing state is covered by Scaling directions; picking distribution keys by Data distribution.
Summary
- Design starts from numbers. The answer to the same question flips with scale
- Users × per-user rate ÷ 86,400 gives average rps
- State the peak multiplier as an assumption. Don't multiply from average for fixed-start-time loads
- Get the read/write ratio. Reads scatter; writes don't
- Volume: 2–3× raw, because indexes and replicas add up
- Precision: orders of magnitude are enough. 2× error doesn't change the design; 10× does
- Estimation's product is being able to discard unneeded setup
- Complexity — Starting point for order-of-magnitude thinking
- Indexes — Why capacity inflates over raw data volume
- Practical AWS Guide — Instance selection and pricing estimates
What to read next
- Scaling directions — When the estimated load doesn't fit on one machine, which way to grow
- Complexity — Count operations inside one machine in orders of magnitude