7. Capacity Estimation and Sizing
Turning a vague idea into numbers, and the numbers into decisions. This expands the short example in Fundamentals. The next notes use these numbers to pick tools: Choosing Data Stores and Choosing a Language.
The method
flowchart LR
A[Users and behaviour] --> B[Requests per second]
B --> C[Data volume and growth]
C --> D[Bandwidth]
D --> E[Memory for cache]
E --> F[Servers and workers]
F --> G[Which tools fit these numbers?]
- Every number is an assumption: write it down, say it aloud, change it when told otherwise.
- Use round numbers.
86,400 s/dayis about100,000, soper day / 100,000 = per second. - Work out average, then peak (usually 2x to 3x average, more for events like sales or live games).
- Estimate reads and writes separately; they scale differently.
- Always finish with "so what?": which design choice does each number force?
Worked example: a photo app with 10M daily active users
Assumptions
$ DAU = 10,000,000
$ uploads per user = 2 / day
$ views per user = 50 photos / day
$ original photo = 2 MB
$ thumbnail / preview = ~200 KB served in feeds
$ metadata per photo = ~1 KB (id, owner, url, caption, time, EXIF)
$ keep data = 5 years
$ peak = 2 x average
1. Request rates
$ uploads / day = 10M x 2 = 20M
$ upload QPS = 20M / 100,000 = ~200 / s (peak ~400 / s)
$ views / day = 10M x 50 = 500M
$ view QPS = 500M / 100,000 = ~5,000 / s (peak ~10,000 / s)
$ read : write = 5,000 : 200 = 25 : 1 -> read heavy
2. Storage
$ originals / day = 20M x 2 MB = 40 TB / day
$ originals / 5 years = 40 TB x 365 x 5 = ~73 PB
$ resized variants = +25% (3 sizes) = ~90 PB logical
$ with durability = x3 copies = ~270 PB raw
$ or erasure coding = ~130 PB raw (about x1.4)
$ metadata / 5 years = 20M x 365 x 5 = 36.5B rows x 1 KB = ~36.5 TB
3. Bandwidth
$ ingress = 200 / s x 2 MB = 400 MB/s = ~3.2 Gbps (peak ~6.4 Gbps)
$ egress = 5,000 / s x 200 KB = 1 GB/s = ~8 Gbps (peak ~16 Gbps)
$ if we served 2 MB originals instead = 10 GB/s = ~80 Gbps <- unaffordable
4. Cache memory (80/20 rule: 20% of items serve 80% of traffic)
$ metadata read volume / day = 500M x 1 KB = ~500 GB
$ hot 20% = ~100 GB -> fits in a few Redis nodes
5. Servers and workers (rough, assumes each app server handles ~1,000 simple requests/s)
$ API servers = (10,000 reads + 400 writes) / 1,000 = ~11
$ with 2x headroom + spread across 3 zones = ~24
$ image resize = 200 / s x 0.5 s CPU each = 100 cores avg, 200 at peak
$ = ~25 machines of 8 cores, autoscaled on queue length
The resulting design
flowchart LR
U[Users] --> CDN[CDN: thumbnails and photos]
U --> API[API servers x ~24]
API -- pre-signed URL --> U
U -- direct upload --> OBJ[(Object storage ~90 PB)]
OBJ --> Q(["Resize queue"])
Q --> W[Workers x ~25]
W --> OBJ
API --> C[(Cache ~100 GB)]
C --> M[(Metadata store ~36 TB, sharded)]
CDN -. miss .-> OBJ
Reusable numbers
| Quantity | Rule of thumb |
|---|---|
| Seconds per day | 86,400, call it 100,000 |
| Seconds per month / year | ~2.5M / ~30M |
| 1M requests/day | ~12 / s |
| 1B requests/day | ~12,000 / s |
| Peak factor | 2x to 3x average |
| Read:write for social apps | 10:1 to 100:1 |
| Text row | ~1 KB; tweet-like post ~300 B |
| Photo / video / HD movie | ~2 MB / ~50 MB per minute / ~4 GB |
| 1 Gbps | ~125 MB/s |
Very rough single-machine limits (order of magnitude, depends on hardware and workload; always measure)
| Component | Ballpark |
|---|---|
| Web/API server (simple requests) | hundreds to a few thousand req/s |
| Relational DB (one node, indexed key access) | thousands to tens of thousands of simple queries/s; tens of millions to low billions of rows; up to a few TB |
| Redis / Memcached node | ~100,000 ops/s, limited by RAM |
| Kafka broker | hundreds of thousands of msgs/s |
| Single disk | SSD thousands of IOPS and hundreds of MB/s |
Use these to ask: does one machine handle it, or must I scale out?
What the numbers tell you
| If you see... | Then consider |
|---|---|
| Data in TB to PB, files or media | Object storage plus metadata DB |
| Reads far above writes | Cache, CDN, read replicas |
| Writes in tens of thousands/s or more | Partitioned stores, queues, append-only logs (Wide-column / time-series) |
| Data fits on one machine and < ~1,000 QPS | A single Postgres. Do not over-engineer |
| Spiky traffic | Queues, autoscaling, serverless |
| Global users | CDN, multi-region replicas, GeoDNS |
| Strict correctness (money) | Relational, transactions, strong consistency |
| Heavy CPU per request (video, ML) | Async workers, queue, separate scaling |
Calculator you can reuse
def size(dau, per_user_writes, per_user_reads, item_bytes, meta_bytes, years, peak=2):
sec = 100_000
w = dau * per_user_writes / sec
r = dau * per_user_reads / sec
day_bytes = dau * per_user_writes * item_bytes
return {
"write_qps": (round(w), round(w * peak)),
"read_qps": (round(r), round(r * peak)),
"storage_PB": day_bytes * 365 * years / 1e15,
"metadata_TB": dau * per_user_writes * 365 * years * meta_bytes / 1e12,
"ingress_MBps": w * item_bytes / 1e6,
}
print(size(10_000_000, 2, 50, 2_000_000, 1_000, 5))
Tools
Estimates tell you where to look; tools tell you if you were right. Workflow: estimate -> benchmark one node -> multiply -> price it -> load test the whole thing -> monitor and correct.
flowchart LR
E[Paper estimate] --> B[Benchmark one node]
B --> M[Scale up: nodes needed]
M --> P[Price in a cost calculator]
P --> L[Load test the real system]
L --> O[Monitor production and re-estimate]
O --> E
| Stage | Tool | Use it for |
|---|---|---|
| Do the sums | Spreadsheet, Python or a notebook (the calculator above) | Changing one assumption and seeing the effect everywhere |
| Sanity-check latency | "Latency Numbers Every Programmer Should Know" tables | Memory vs SSD vs network costs |
| Benchmark the app | k6, Locust, wrk, hey, JMeter, Gatling | Requests/s one server handles; latency under load |
| Benchmark the database | pgbench (Postgres), sysbench, YCSB, cassandra-stress |
Queries/s and latency on your schema and hardware |
| Benchmark disk and network | fio, iperf3 | IOPS, throughput, bandwidth between machines |
| Price it | AWS Pricing Calculator, Azure Pricing Calculator, Google Cloud Pricing Calculator | Turning "24 servers, 90 PB, 8 Gbps egress" into a monthly bill |
| Measure real usage | Prometheus and Grafana, cloud monitoring dashboards | Replacing assumptions with real QPS, sizes and peaks |
| Right-size containers | Kubernetes HPA/VPA, tools like Goldilocks or Kubecost | CPU and memory requests that match reality |
| Draw it | Mermaid, Excalidraw, draw.io | Showing the design the numbers produced |
Commands to start with
$ k6 run --vus 100 --duration 30s script.js # scripted load, JS test files
$ wrk -t4 -c100 -d30s http://localhost:8080/health # quick HTTP throughput test
$ hey -z 30s -c 50 http://localhost:8080/ # simple load generator
$ locust -f locustfile.py --headless -u 200 -r 20 # Python user-behaviour tests
$ pgbench -i -s 100 appdb # create test data (scale 100)
$ pgbench -c 32 -j 4 -T 60 appdb # 32 clients, 60 seconds
$ fio --name=randread --rw=randread --bs=4k --size=1G \
--iodepth=32 --ioengine=libaio --direct=1 --runtime=30 --time_based
$ iperf3 -s # on machine A
$ iperf3 -c <machine-A-ip> # on machine B: network speed
Little's Law: the quick formula behind most sizing.
$ concurrent requests = arrival rate x time in system
$ 1,000 req/s x 0.1 s = 100 requests in flight
$ so a server with a 50-connection pool needs 2 servers, or faster queries
Tips - Benchmark with realistic data and request mixes. An empty database and one endpoint hide most problems. - Run the load generator on a separate machine, or it competes for the same CPU. - Watch percentiles (p95, p99), not only the average. - Find the knee: raise load step by step until latency climbs sharply. Run at about 50% to 70% of that point for headroom. - Test the peak plus failure: kill a node mid-test. - Keep results with the hardware and config used so you can compare later.
Common mistakes
- Estimating only the average and not the peak.
- Forgetting replication (x3) and growth (usage and size rise every year).
- Mixing bits and bytes (Gbps vs GB/s).
- Treating storage and bandwidth as the same problem.
- Designing for 100x the realistic load on day one. Size for roughly 1 to 2 years ahead and keep a path to scale.