System Design · lesson 4 of 11
Beginner
4Intermediate
4Advanced
3Also part of System Design
12- + Scaling Patterns: Real Sharding vs a Table That Only Looks Sharded from Databases
- + Killing the Primary and Measuring the Real Failover Time from MongoDB
- + ACID vs BASE: the Lost Update Neither Engine Prevents by Default from Databases
- + Distributed Correctness in Interviews from Interview Preparation
- + Events vs Requests: Why the Order Service Doesn't Wait for Email from Event-Driven Architecture
- + Scaling, Queues, and Backpressure from Cloud Interviews
- + Cache-Aside with Redis: Stale Reads, Races and Stampedes from Redis
- + Circuit Breaker: Stop Calling a Service That's Already Down from Microservices
- + API Gateway: Aggregating Calls With a Fallback from Microservices
- + Observability Synthesis: Logs, Metrics, Traces, and Alerts as One Signal from DevOps
- + Design a URL Shortener: Sequential Codes vs Real Hash Collisions from Low-Level Design
- + Availability, Regions, and the Arithmetic of Nines from Cloud Interviews
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
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.