Module 1 · Interview Method and the Request Path · Lesson 2 of 20
Back-of-the-Envelope Estimation: QPS, Storage, Bandwidth and Latency
Learning outcome
You can turn "10 million daily users" into average and peak requests per second, a read/write ratio, storage growth per year, bandwidth, cache size and an instance count in about five minutes, out loud, with round numbers. You know the latency numbers every engineer should recognise, and you can use them to tell whether a design can meet its p99 target before you build it.
Intuition
Estimation is not about being exact. It is about knowing which order of magnitude you are in, because each order of magnitude needs a different design. Ten requests per second fits on one small app service. A thousand needs a few instances and a good database. A hundred thousand needs caching, replicas and probably partitioning. The estimate tells you which conversation to have.
The trick is that a day has about 100,000 seconds (86,400). So "X per day" divided by 100,000 is "X per second", near enough. One million requests a day is about 10 per second (11.6 exactly). One billion a day is about 10,000 per second.
flowchart LR
U["Daily users x actions per user"] --> D["Requests per day"]
D -->|"/ 86,400"| A["Average QPS"]
A -->|"x peak factor (2-10)"| P["Peak QPS"]
P --> I["Instances = peak / per-instance capacity + spare"]
D --> S["Storage = writes per day x bytes x 365"]
P --> B["Bandwidth = peak QPS x response size"]Deep dive
The five numbers to produce
- Average QPS for reads and writes separately: daily actions / 86,400.
- Peak QPS: average times a peak factor. Consumer apps often see 2-3x at the daily peak and 5-10x or more at sales or launches. Ask, or state your assumption.
- Read/write ratio: it decides whether caching and replicas help (read-heavy) or whether you need write scaling (write-heavy).
- Storage growth: writes per day x bytes per record x 365, then multiply by replicas and backups. Include indexes; a rule of thumb is 1.5-2x the raw row size.
- Bandwidth: peak QPS x average response size. It drives CDN and egress cost.
Powers of two and ten
| Unit | Approximate value |
|---|---|
| 1 KB | 1,000 bytes (a small JSON object) |
| 1 MB | 1,000 KB (a photo) |
| 1 GB | 1,000 MB |
| 1 TB | 1,000 GB (a large single database) |
| 1 PB | 1,000 TB |
| seconds per day | 86,400 (round to 100,000) |
| seconds per month | about 2.6 million |
| seconds per year | about 31.5 million |
Latency numbers worth knowing
These are orders of magnitude, not benchmarks. They explain why caches, data locality and fewer round trips matter.
| Operation | Typical latency |
|---|---|
| L1 cache reference | about 1 ns |
| Main memory reference | about 100 ns |
In-process dictionary or IMemoryCache lookup | well under 1 microsecond |
| Read 1 MB sequentially from memory | about 10-50 microseconds |
| SSD random read | about 100 microseconds |
| Round trip inside one datacenter / availability zone | about 0.5 ms |
| Redis GET over the network in the same region | about 0.5-1 ms |
| Simple indexed SQL query, same region | about 1-5 ms |
| Round trip between Azure regions on one continent | about 10-40 ms |
| Round trip India to US East | about 200 ms |
Two consequences you can say in an interview:
- Round trips dominate. An API that makes 30 sequential database calls at 2 ms each spends 60 ms before doing any work. Batch, join, or cache.
- Cross-region calls belong off the hot path. A 200 ms round trip inside a request with a 300 ms p99 budget leaves almost nothing for the rest.
Latency budgets
Split the p99 target across the hops. For GET /orders/{id} with p99 < 300 ms: TLS and edge 20 ms, gateway 5 ms, API work 20 ms, cache 2 ms, database on a miss 20 ms, leaving a large margin. For POST /orders with a payment call that alone has p99 of 600 ms, the 800 ms budget is tight. That is the signal to make the email and the warehouse update asynchronous (Lesson 10).
Capacity per instance
Do not guess how many requests an ASP.NET Core instance can serve: it depends on what each request does. Say "I would load-test one instance at our target CPU (say 60%) and measure". For an I/O-bound JSON API that mostly waits on a database or cache, a few thousand requests per second per instance is common. Always add at least one spare instance so you survive a failure or a rolling deploy.
Code example: an estimation calculator
The program below encodes the estimate for the online shop from Lesson 1. Every constant at the top is an assumption you would say out loud.
// Back-of-the-envelope estimate for a .NET shop API. Every input is an assumption you state out loud.
const double dau = 10_000_000; // daily active users
const double readsPerUser = 30; // product/catalog page API calls per user per day
const double ordersPerUser = 0.2; // one order every five days on average
const double peakFactor = 3; // evening/sale peak vs daily average
const double orderBytes = 2_000; // order row + lines + indexes, bytes
const double readResponseBytes = 8_000; // average JSON response, bytes
const double rpsPerInstance = 1_500; // measured by a load test on one API instance at 60% CPU
const double hotFraction = 0.2; // share of products that serve most reads
const double products = 2_000_000;
const double productBytes = 4_000;
const double secondsPerDay = 86_400;
double readQps = dau * readsPerUser / secondsPerDay;
double writeQps = dau * ordersPerUser / secondsPerDay;
double peakRead = readQps * peakFactor, peakWrite = writeQps * peakFactor;
double ordersPerYear = dau * ordersPerUser * 365;
double storageYearGb = ordersPerYear * orderBytes / 1e9;
double egressMbps = peakRead * readResponseBytes * 8 / 1e6;
int instances = (int)Math.Ceiling(peakRead / rpsPerInstance) + 1; // +1 so one instance can fail or be deployed
double cacheGb = products * hotFraction * productBytes / 1e9;
Console.WriteLine($"average read QPS {readQps,12:N0}");
Console.WriteLine($"average write QPS {writeQps,12:N0}");
Console.WriteLine($"peak read QPS (x{peakFactor}) {peakRead,12:N0}");
Console.WriteLine($"peak write QPS (x{peakFactor}) {peakWrite,12:N0}");
Console.WriteLine($"read:write ratio {readQps / writeQps,12:N0} : 1");
Console.WriteLine($"orders per year {ordersPerYear,12:N0}");
Console.WriteLine($"order storage / year {storageYearGb,12:N0} GB (before replicas and backups)");
Console.WriteLine($"peak egress {egressMbps,12:N0} Mbit/s");
Console.WriteLine($"API instances {instances,12} (peak / {rpsPerInstance:N0} rps, plus one spare)");
Console.WriteLine($"hot product cache {cacheGb,12:N1} GB");Output (dotnet run, .NET 10):
average read QPS 3,472 average write QPS 23 peak read QPS (x3) 10,417 peak write QPS (x3) 69 read:write ratio 150 : 1 orders per year 730,000,000 order storage / year 1,460 GB (before replicas and backups) peak egress 667 Mbit/s API instances 8 (peak / 1,500 rps, plus one spare) hot product cache 1.6 GB
What to notice:
- 150:1 read/write. The system is read-heavy, so caching product and order reads gives the biggest win, and one primary database can easily take about 70 writes per second.
- About 10,000 reads per second at peak. That is beyond what you want a single database to serve directly, so reads go through a cache (Lesson 8), and history reads can go to a replica (Lesson 6).
- 1.5 TB of orders a year, before replicas and backups. Three years on one relational database is fine. If the interviewer says 10x, you are at 15 TB a year and partitioning (Lesson 7) or archiving old orders becomes part of the design.
- 667 Mbit/s of egress at peak is a cost line and a reason to compress responses and serve images from a CDN.
- 8 instances comes from a measured 1,500 requests per second per instance, plus one spare. Change the measured capacity and the answer changes, which is why you measure.
- 1.6 GB of hot product data fits in one Redis node, or even in each instance's memory as an L1 cache.
Failure modes
- False precision. "3,472.2 requests per second" wastes time. Say "about 3,500, call it 4,000 with headroom".
- Forgetting the peak. Systems fail at the peak, not on average. Always multiply.
- Forgetting replicas, indexes and backups in storage. Raw rows are often less than half the real footprint.
- Treating latency as additive only. Parallel calls cost the slowest call, not the sum. Sequential calls cost the sum. Say which one your design does.
- Mixing up bits and bytes. Network bandwidth is quoted in bits per second; multiply bytes by 8.
- Assuming capacity per instance. Measure with a load test; state the number as an assumption until you have.
Interview angle
Interviewers do not check your arithmetic to the decimal. They check that you produce the numbers, round sensibly, and use them. The pattern is: state the assumption, compute, then draw a conclusion. "10 million daily users, 30 reads each, is 300 million a day, about 3,500 per second on average and about 10,000 at a 3x peak. That is read-heavy at 150 to 1, so I will put a cache in front and keep writes on one primary."
If you are given different numbers halfway through, redo the estimate quickly. Being willing to update the design when the numbers change is exactly what the interviewer is testing.
Practice questions
- A service receives 50 million requests per day. What is the average QPS?
Answer: About 580 per second (50,000,000 / 86,400). Call it 600, and plan for a peak of 1,500-3,000 depending on the traffic shape.
- Each order is 2 KB including indexes, and you store 1 million orders per day. How much storage per year?
Answer: 2 GB per day, about 730 GB per year, before replicas and backups. With one replica and backups, plan for roughly 2 TB.
- A page makes 12 sequential calls to Redis at 1 ms each and 4 sequential SQL queries at 3 ms each. What is the data-access time, and how do you cut it?
Answer: About 24 ms. Batch the Redis reads (MGET or a pipeline), run independent calls in parallel with Task.WhenAll, and combine queries. Parallel calls cost roughly the slowest one.
- Why add a spare instance to the computed instance count?
Answer: So the system still meets its peak when one instance fails or is being redeployed. N+1 (or N+2 across zones) is the minimum for availability.
- The peak response is 8 KB and peak QPS is 10,000. What bandwidth do you need?
Answer: 80 MB/s, which is 640 Mbit/s. Compression and a CDN for static content reduce it.
Revision checklist
- 86,400 seconds per day; 1 million per day is about 12 per second.
- Peak = average x 2-10; state the factor.
- Storage = writes x size x 365, then x replicas and backups.
- Bandwidth in bits: bytes x 8.
- Memory 100 ns, same-zone round trip 0.5 ms, cross-continent 100-200 ms.
- Instance count comes from a load test, plus a spare.
Related on TechInterviewNotes
- Previous lesson: How to Run a System Design Interview as a .NET Developer
- Next lesson: The Request Path: DNS, CDN, Load Balancers and API Gateways
Analogy
Planning a wedding
You do not count every guest's appetite. You say "300 guests, about 3 plates each, so roughly 1,000 plates, and the rush is at 8 pm, so we need enough servers for half of them in an hour". Rough numbers decide whether you hire one caterer or three. Estimation in system design is the same: round numbers that tell you how big the kitchen must be.
Quick reference
Formulas
- Average QPS = actions per day / 86,400.
- Peak QPS = average x peak factor.
- Storage per year = writes per day x bytes x 365 x (replicas + backups factor).
- Bandwidth (bit/s) = peak QPS x response bytes x 8.
- Instances = ceil(peak QPS / measured QPS per instance) + 1.
- Cache size = hot items x item size.