Requirements & Capacity Estimation¶
Prerequisites: HTTP basics, System Design Framework (pair this with steps 3–5)
← Foundations | Next: Stateless vs Stateful →
Why This Exists¶
"Design WhatsApp." Two candidates draw the same boxes. Only one can say:
2B users, 50% DAU → 1B DAU
40 msgs/user/day → 40B msgs/day
40B / 86,400 → ~460k avg msg/s
peak 5–10× → 2–5M msg/s
100 B text → 40 TB/day raw
RF=3 + media → this is a storage company that also chats
Without those numbers you cannot choose Postgres vs a log, one region vs three, or a 4-core box vs a fleet. Estimation is not accounting — it is which physics applies.
Mental Model
Capacity is a funnel. DAU × actions/day becomes QPS. QPS × bytes becomes bandwidth and disk. Disk × RF × regions becomes the invoice. Cache hit rate is the only free lunch, and it is never free at miss.
DAU → daily reqs → avg QPS → peak QPS → R/W split → bytes → disk/net → connections
Naive System → What Breaks¶
You hear "10 million users" and size a single 16-core API + one Postgres primary.
| Missed factor | What actually happens |
|---|---|
| 10M DAU, not registered | If registered is 80M, you undersized 8× |
| 20 req/user/day | 200M req/day ≈ 2,300 avg QPS — fine |
| Peak 8–20× (lunch, launch) | 20k–45k QPS — the primary's pool of 100 is gone |
| 90% reads | You needed replicas / cache, not a bigger primary |
| 2 KB payload | Peak 20k × 2 KB = 40 MB/s — trivial. 1 MB photos? 20 GB/s |
| RF=3, 3 regions | Storage × 9. "30 GB" became 270 GB plus snapshots |
The naive system dies at connections and tails, not at "we ran out of CPU."
The Concept¶
Functional requirements are what the product does. Non-functional requirements (NFRs) are how wrong / how slow / how often that is allowed.
WhatsApp-shaped example:
| Kind | Requirement | Design consequence |
|---|---|---|
| Functional | 1:1 and group text, online ticks, media | Chat store + presence + blob store |
| Functional | Message delivered while receiver offline | Durable queue, not RAM |
| NFR | p99 send-ack < 400ms on-net | Co-located region, no cross-DC sync write |
| NFR | No silent loss (durability) | fsync / quorum before ACK |
| NFR | 99.99% message API | Multi-AZ, not multi-region sync |
| NFR | Last 30 days on device + cloud | Hot tier vs cold object store |
If you list "highly available and fast" you have not written NFRs. Write numbers.
Interview Insight 🎯
Interviewers listen for the sentence: "That's a functional requirement — it adds an entity. This other one is an NFR — it adds a replica, a cache, or a queue." Mixing them is how designs grow random boxes.
Architecture¶
flowchart LR
DAU[DAU] -->|× actions/day| DAY[Reqs / day]
DAY -->|/ 86400| AVG[Avg QPS]
AVG -->|× peak| PEAK[Peak QPS]
PEAK -->|read %| RW[Read QPS / Write QPS]
RW -->|× payload| BW[Bandwidth]
RW -->|write × 86400 × bytes| ST[Storage / day]
ST -->|× 365 × RF × regions| YR[Stored / year]
RW -->|× hit %| CACHE[Cache vs origin]
PEAK -->|× conns/req| CONN[LB / pool / FD]
style PEAK fill:#e65100,color:#fff
style YR fill:#b71c1c,color:#fff
style CACHE fill:#1565c0,color:#fff Mechanics¶
Seconds in a day = 86,400. Memorize 10^5 ≈ day. N / 10^5 is avg QPS within 15%.
10M DAU × 20 req/user/day = 200M req/day
200M / 86,400 ≈ 2,315 avg QPS
peak multiplier 8–10× ≈ 18–23k peak QPS
read 90% ≈ 21k read / 2.3k write at peak
payload 2 KB ≈ 23k × 2 KB ≈ 46 MB/s peak
Reads vs writes. Writes hit the source of truth and the WAL. Reads hit cache, then replicas. A 90/10 split is a cache problem. A 10/90 split (ingest, telemetry) is a log problem.
Payload. Control plane (JSON 1–2 KB) and data plane (photos, video chunks) must be estimated separately. One 1 MB avatar upload at 1% of users/day swamps the 2 KB chat ACKs.
Bandwidth both ways. Response often larger than request (feeds). Count QPS × (in + out).
Storage.
Do not forget indexes, WAL, snapshots, and "deleted" rows you still keep for 30 days.
Replication. RF=3 is three copies plus repair traffic. Cross-region RF is a latency and conflict decision, not just a multiplier.
Cache. Origin QPS ≈ read_QPS × (1 − hit_rate). 80% hit on 21k read QPS → 4.2k origin. 50% hit → 10.5k. The DB you buy is the miss fleet.
Connections.
20k QPS × 0.08s = 1,600 in-flight. 20 pods → 80 conns each — fine. p99 = 2s under GC → 40k in-flight → pool wait → death spiral. See Tail Latency.
Realistic Example With Numbers¶
Feed service: 10M DAU, 20 req/user/day, peak ×8, 90% read, 2 KB, 1 region, RF=3, 80% cache hit.
Daily reqs 200,000,000
Avg QPS 2,315
Peak QPS 18,519
Peak read / write 16,667 / 1,852
Cache hits 13,333 QPS
Origin reads 3,333 QPS
Peak NIC (in+out) ~74 MB/s (2 KB each way)
Avg write QPS 2,315 × 10% ≈ 232 write/s (peak write QPS is 1,852)
Writes / day 232 write/s × 86,400 × 2 KB ≈ 40 GB/day (use average, not peak)
Year logical 40 GB × 365 ≈ 14.6 TB
With RF=3 ~44 TB (+indexes/WAL → budget ~60 TB)
In-flight @ 80ms 18.5k × 0.08 ≈ 1,480
Estimates, not accounting. Being within 2–3× is a pass. Being off by 100× (forgot peak, or stored every read) is a fail.
Production Trap
Peak is not "a bit more than average." Consumer apps see 5–10×. Market open, drop drops, and ticket onsale see 20–50×. If you provision for average, you provision for the incident.
Interactive Explainer¶
Change DAU, peak, payload, RF, and hit rate. Flags fire when a naive single-primary layout is no longer a safe default — they name a bottleneck, not a mandatory mechanism.
Failure Modes¶
| Miss | Symptom in prod | Fix in the estimate |
|---|---|---|
| Used registered users as DAU | Fleet idle, then Saturday melts | DAU + MAU/DAU ratio |
| Forgot peak | p99 death at 12:05, CPU 35% at 04:00 | Peak multiplier by product |
| Stored reads | 100 TB "capacity" plan | Writes only + compaction |
| Hit rate 99% fantasy | Origin 10× the plan on launch | Sensitivity: 50 / 80 / 95% |
| One connection per QPS | too many clients at 5k QPS | Little's Law + pooling |
| Payload 2 KB including media | Egress bill 50× | Split control vs blob |
Production Debugging¶
Capacity mistakes show up as saturation, not as a wrong spreadsheet.
CPU high + p99 high → you undersized compute; scale out
CPU low + p99 high → not a capacity miss — tail, lock, dep
Memory OOM / cache evict → working set > RAM; hit rate lie
Disk WAL / log fill → writes/day × RF you forgot
Network NIC 80% + retrans → payload × QPS; jumbo / compression
Queue depth Kafka / SQS climb → consumer QPS < peak write QPS
Lag seconds growing → same; partitions or slow handler
Pools wait > 10ms → in-flight = QPS × latency; add pods or cut p99
p50/p95/p99 p50 ok p99 5s → see tail latency; averages hide this
Error rate 503/429 → limiter or overload; you hit a real limit
Timeouts climbing with load → pool + retry amplification
Retries downstream RPS 3× → your estimate used client QPS, not amplified
GC pause == p99 → heap sized for peak live set, not avg
Locks row / global → write QPS concentrated on one key
Scaling Limits¶
- A single 16-core app pod: ~5–15k simple QPS. 20k peak ⇒ more than one pod before Kafka.
- One Postgres primary: often quoted in the low-single-digit k durable writes/s as an interview-estimation anchor, not a limit — real capacity depends on the workload; sharding or a log is not the automatic next step.
- One Redis shard: 50–150k simple GET/s; hot key is the real limit, not the average.
- NIC: 1–2 GB/s before you think "this is a CDN / blob problem."
- File descriptors and conntrack fill before CPU at chat-scale connection counts.
- Multi-region RF turns every write into a WAN RTT unless you accept eventual.
Trade-offs¶
| Dimension | Conservative (×3 headroom) | Tight (on-demand) |
|---|---|---|
| Latency | Stable p99 | Fine until peak |
| Throughput | Idle 70% of day | Throttle at peak |
| Availability | Survive 2× surprise | First launch is an incident |
| Consistency | Extra replicas | Read-your-write only on primary |
| Durability | RF=3 + backups | RF=2, cheaper, scarier |
| Complexity | More boxes day one | Re-shard under fire |
| Cost | Overpay 2–3× | Overpay in pages |
| Ops | Boring capacity reviews | Heroic evenings |
Interview Questions¶
Q: 10M DAU, 20 requests/user/day. What is average and peak QPS?
"200M requests/day ÷ 86,400 ≈ 2,300 average QPS. I assume a peak multiplier of 8–10× for a consumer app, so 18–23k peak. I would ask about launch spikes and whether this is read-heavy before I pick a database."
Q: Same numbers, 90% read, 2 KB, 80% cache hit. Size the database.
"Peak 20k QPS → ~18k reads, ~2k writes. Cache serves ~14k reads; origin sees ~4k reads and 2k writes. Writes/day ≈ 230/s × 86k × 2 KB ≈ 40 GB/day, ~15 TB/year logical, ~45 TB at RF=3. I would not shard Postgres on day one at 2k durable writes/s — I would put a cache in front and plan replicas for the 4k origin reads. I'd also sanity-check payload: if 10% of requests are images, the NIC story changes completely."
Q: Finance says your 3-region RF=3 plan is 9× storage. They want RF=2, one region. What do you negotiate?
"Split the data classes. User passwords and ledger rows: RF=3, multi-AZ, backups with tested RPO. Chat media: RF=2 in object storage, one region + async copy, 24h RPO. Presence: RF=1, ephemeral. Then I show the real cost driver is often egress and SMS, not disks. I will not 'save money' by dropping quorum on the money path. I'd also replace a blanket 9× with: RF=3 locally, async cross-region for DR — that's ~4× plus lag, which matches a 1-hour RPO."
Reasoning Exercises¶
- WhatsApp: 1B DAU, 40 msgs/user/day, 80% text (100 B), 20% media (200 KB). Compute daily storage and peak msg/s. Which component do you scale first?
- Your cache hit rate drops from 90% to 60% after a deploy (key namespace change). Using the calculator defaults, what happens to origin QPS and to the primary?
- A trading venue: 2k avg QPS, peak 80× at open. Why is "size for 2k" malpractice? What NFR do you write instead?
- 10k long-lived WebSockets per box, 200k users online. How many boxes for connections only? How does that differ from the QPS fleet?
Key Takeaways¶
Remember
- Functional reqs add features; NFRs add replicas, caches, queues, and SLOs — write numbers.
DAU × req/day / 86,400 × peakis the only QPS formula you need.- Size the miss path and the write path; hits are a discount, not a plan.
- RF × regions × indexes is the storage number finance will see.
- Estimates within 2–3× are useful; 100× misses come from forgetting peak, payload, or what you actually persist.
Previous: Foundations | Next: Stateless vs Stateful
Staff Engineer Lens
Capacity reviews fail when they are annual spreadsheets. Tie estimates to SLIs: if p99 or origin QPS leaves the envelope, the estimate was a hypothesis and production falsified it. The staff move is a sensitivity table (hit 50/80/95, peak 5/10/20) in the design doc — not a single bold number.
Interview Insight 🎯
Say the formula before the arithmetic. Interviewers forgive 86,400 ≈ 10^5. They do not forgive skipping peak, read/write split, or "I'll cache it" with no hit-rate assumption.