Skip to main content
System Design · lesson 4 of 11
Open the System Design page →
System Design beginner Lesson 4 of 11

Capacity Estimation: Turning Assumptions Into Real Numbers

Back-of-envelope math for QPS, storage, and bandwidth from stated assumptions, computed as real numbers instead of described in prose.

By SkillByExample · Published

system-design capacity-planning python

Every other system-design lesson assumes some scale; this one is where those numbers actually come from — real arithmetic on stated assumptions, not a description of what “estimate capacity” means.

From assumptions to QPS, storage, and bandwidth

daily_active_users = 2_000_000
requests_per_user_per_day = 15
avg_payload_bytes = 2_048
peak_multiplier = 3

total_requests_per_day = daily_active_users * requests_per_user_per_day
avg_qps = total_requests_per_day / 86_400
peak_qps = avg_qps * peak_multiplier

daily_storage_bytes = total_requests_per_day * avg_payload_bytes
daily_storage_gb = daily_storage_bytes / (1024 ** 3)
yearly_storage_tb = (daily_storage_gb * 365) / 1024

avg_bandwidth_mbps = (avg_qps * avg_payload_bytes * 8) / (1024 ** 2)
peak_bandwidth_mbps = avg_bandwidth_mbps * peak_multiplier

print(f"Total requests/day:   {total_requests_per_day:,}")
print(f"Average QPS:          {avg_qps:,.1f}")
print(f"Peak QPS ({peak_multiplier}x):        {peak_qps:,.1f}")
print(f"Daily storage:        {daily_storage_gb:,.2f} GB")
print(f"Yearly storage:       {yearly_storage_tb:,.2f} TB")
print(f"Average bandwidth:    {avg_bandwidth_mbps:,.2f} Mbps")
print(f"Peak bandwidth:       {peak_bandwidth_mbps:,.2f} Mbps")
$ python capacity_estimate.py
Total requests/day:   30,000,000
Average QPS:          347.2
Peak QPS (3x):        1,041.7
Daily storage:        57.22 GB
Yearly storage:       20.40 TB
Average bandwidth:    5.43 Mbps
Peak bandwidth:       16.28 Mbps

Four stated assumptions — 2 million daily users, 15 requests each, a 2 KB average payload, traffic peaking at 3x average — turn into seven concrete numbers. Every one of those numbers is challengeable: disagree with peak_multiplier and the peak QPS/bandwidth figures move with it, but the arithmetic connecting them doesn’t need re-deriving each time.

Practice

1. Re-run the estimate for a service 25x larger (50 million DAU) and see which numbers scale linearly and which don't.
def estimate(dau, req_per_user, payload_bytes, peak_multiplier=3):
    total = dau * req_per_user
    avg_qps = total / 86_400
    storage_gb = (total * payload_bytes) / (1024 ** 3)
    return avg_qps, avg_qps * peak_multiplier, storage_gb

for dau in [2_000_000, 50_000_000]:
    avg_qps, peak_qps, storage_gb = estimate(dau, 15, 2_048)
    print(f"DAU {dau:>12,}: avg QPS {avg_qps:>10,.1f}  peak QPS {peak_qps:>10,.1f}  daily storage {storage_gb:>10,.2f} GB")
$ python scale_up.py
DAU    2,000,000: avg QPS      347.2  peak QPS    1,041.7  daily storage      57.22 GB
DAU   50,000,000: avg QPS    8,680.6  peak QPS   26,041.7  daily storage   1,430.51 GB

Every number scales linearly with dau here, because nothing in this formula has a fixed cost independent of user count — a real system usually has some fixed overhead (base connection pools, index sizes) that would make the 25x case less than exactly 25x in practice.

2. Split total requests into a 90/10 read/write ratio and compute QPS for each separately.
daily_active_users = 2_000_000
requests_per_user_per_day = 15
read_ratio = 0.9

total_requests = daily_active_users * requests_per_user_per_day
read_requests = total_requests * read_ratio
write_requests = total_requests * (1 - read_ratio)

read_qps = read_requests / 86_400
write_qps = write_requests / 86_400

print(f"Total requests/day: {total_requests:,}")
print(f"Read QPS (90%):     {read_qps:,.1f}")
print(f"Write QPS (10%):    {write_qps:,.1f}")
print(f"Read:Write ratio:   {read_qps / write_qps:.1f}:1")
$ python read_write_split.py
Total requests/day: 30,000,000
Read QPS (90%):     312.5
Write QPS (10%):    34.7
Read:Write ratio:   9.0:1

The read/write split matters because reads and writes usually scale differently — reads often get cheaper per-request with caching, while writes are frequently the harder bottleneck (a single primary database, replication lag), so knowing the split changes which side of the system needs the scaling work.

3. Compute how many servers are needed at peak QPS, with and without a 30% headroom margin.
import math

peak_qps = 1_041.7
qps_per_server = 400
overhead_factor = 1.3  # keep headroom, don't run servers at 100% of capacity

servers_needed_raw = peak_qps / qps_per_server
servers_needed_with_headroom = (peak_qps * overhead_factor) / qps_per_server

print(f"Peak QPS:                      {peak_qps:,.1f}")
print(f"Servers needed (no headroom):   {math.ceil(servers_needed_raw)}")
print(f"Servers needed (30% headroom):  {math.ceil(servers_needed_with_headroom)}")
$ python servers_needed.py
Peak QPS:                      1,041.7
Servers needed (no headroom):   3
Servers needed (30% headroom):  4

The headroom margin is the difference between 3 and 4 servers here — a small percentage assumption changes a concrete provisioning decision, which is exactly why it should be a stated number in a design doc rather than left implicit.

4. Show how the peak QPS estimate changes across a range of peak multipliers (2x to 10x).
avg_qps = 347.2

for multiplier in [2, 3, 5, 10]:
    peak = avg_qps * multiplier
    print(f"Peak multiplier {multiplier:>2}x: peak QPS = {peak:,.1f}")
$ python multiplier_sensitivity.py
Peak multiplier  2x: peak QPS = 694.4
Peak multiplier  3x: peak QPS = 1,041.6
Peak multiplier  5x: peak QPS = 1,736.0
Peak multiplier 10x: peak QPS = 3,472.0

A service with genuinely spiky traffic (a ticket sale, a breaking-news app) justifies a higher multiplier than one with steady usage — the number itself is a claim about the traffic shape, worth stating and defending, not a constant to copy from one estimate to the next.

Frequently Asked Questions

Why does a peak multiplier matter if I already know the average QPS?
Because provisioning for the average leaves no headroom for the traffic pattern every real service actually has — a spike at a predictable hour, a flash sale, a viral post. A peak multiplier (commonly 2-5x average, sometimes higher) is a stated assumption, not a guess dressed up as one: state it explicitly so a reviewer can challenge the number instead of the arithmetic.
Where do the input numbers (DAU, requests/user, payload size) actually come from in a real interview or design doc?
Either given directly by whoever is asking, or estimated from a comparable known service and stated as an assumption. The point of this lesson isn't guessing correctly — it's that once the assumptions are stated, the rest is arithmetic anyone can check and challenge, not a vibe.
Does this replace load testing?
No — it's a sanity check that happens before any infrastructure exists, to catch a design that's off by 10x or 100x before money or time is spent. A real load test against real traffic is still how you'd verify the estimate once something is built.
Why compute storage per year instead of just per day?
A daily number that looks trivial (57 GB) can become a real budget line and retention-policy decision at a year's scale (20+ TB) — the yearly figure is what actually shows up in a cost conversation or a 'how long can we afford to keep this' decision.