“Let’s estimate the scale first” is the point in a system design interview where many candidates either freeze or produce a number with seven significant figures and no visible reasoning. Neither helps. A capacity estimate isn’t about getting the right answer, since nobody in the room knows it. It’s about turning a few stated assumptions into numbers big enough or small enough to change the design: one database or fifty, a CDN or not, keep everything forever or tier it to cold storage.
This guide works through one estimate end to end, a photo-sharing app, then covers the mistakes that make estimates useless.
Shortcuts worth memorising
A few conversions make the arithmetic fast enough to do out loud:
- A day has 86,400 seconds. Round it to 100,000 for quick work, and remember that rounding understates per-second rates by about 15%.
- One million events per day is about 12 per second.
- A month is about 2.6 million seconds.
- Bytes to bits: multiply by 8. Network links are sold in bits per second, storage in bytes.
- KB, MB, GB, TB, PB each step up by 1,000 (close enough to 1,024 for estimates).
You don’t need more than that. Precise numbers like 34,722 requests per second should be said as “about 35,000”.
Working through a photo-sharing app
Start with assumptions, said out loud and written down where the interviewer can see them. Each one is a guess you can defend, and each one can be changed:
| Assumption | Value | Why it’s reasonable |
|---|---|---|
| Daily active users | 50 million | The interviewer’s prompt, or a mid-sized social app |
| Photos viewed per user per day | 60 | A few minutes of scrolling |
| Photos uploaded per user per day | 0.2 | One in five users posts daily |
| Peak hour vs daily average | 3x | Traffic bunches into evenings |
| Stored original | 2 MB | Phone photo after compression |
| Derived sizes (thumbnail, feed, full screen) | 0.8 MB total | Generated after upload |
| Size of a typical view | 150 KB | Most views load the feed size, not the original |
| Metadata per photo | 1 KB | IDs, caption, timestamps, counters |
| Copies of each blob | 3 | Replication for durability |
Then the arithmetic. It’s short enough to do on a whiteboard, but here it is as a Python script so every number below is reproducible:
DAY = 86_400
dau = 50e6
views_per_user = 60
uploads_per_user = 0.2
peak_factor = 3
original_mb, derived_mb = 2.0, 0.8
view_kb, meta_kb, replicas = 150, 1, 3
uploads = dau * uploads_per_user
views = dau * views_per_user
print(f"upload rate avg {uploads/DAY:,.0f}/s peak {uploads/DAY*peak_factor:,.0f}/s")
print(f"view rate avg {views/DAY:,.0f}/s peak {views/DAY*peak_factor:,.0f}/s")
blob_tb_day = uploads * (original_mb + derived_mb) / 1e6
print(f"new blob storage {blob_tb_day:.0f} TB/day, x{replicas} copies = {blob_tb_day*365*replicas/1000:.0f} PB/year raw")
egress_gbps = views/DAY * view_kb * 1e3 * 8 / 1e9
print(f"egress avg {egress_gbps:.0f} Gbit/s peak {egress_gbps*peak_factor:.0f} Gbit/s")The full script prints a few more lines:
uploads/day 10 M
upload rate avg 116/s peak 347/s
view rate avg 34,722/s peak 104,167/s
read:write ratio 300:1
new blob storage 28 TB/day, 10.2 PB/year, x3 copies = 31 PB/year raw
metadata 10 GB/day, 3.65 TB/year
egress avg 42 Gbit/s peak 125 Gbit/s
API servers at peak 52 (at 2000 req/s each)What the numbers say about the design
The numbers only matter if they change a decision. Read them one at a time.
Uploads are tiny: about 350 per second at peak. That’s a modest write load. A single well-indexed database could take the metadata inserts. The upload path’s hard part is moving 2 MB files reliably, not request rate, which points towards direct-to-object-storage uploads with pre-signed URLs rather than streaming bytes through API servers.
Views are about 100,000 per second at peak, a 300:1 read-to-write ratio. That’s the number that shapes the architecture. Reads need caching, read replicas or a denormalised feed store. Optimising the write path first would be effort in the wrong place.
125 Gbit/s of egress at peak. No sensible origin fleet serves that directly. Images must come from a CDN, and the cache hit ratio at the CDN becomes a cost and capacity metric. At a 95% hit ratio the origin sees about 6 Gbit/s; at 99%, about 1.3 Gbit/s.
31 PB of raw blob storage per year. Metadata is only 3.65 TB per year, small enough to fit on one large database server with room to spare, while the blobs need object storage. At this volume, storage classes matter: photos older than a few months are rarely viewed, so moving them to an infrequent-access tier, or using erasure coding (around 1.5x overhead instead of 3x) instead of three full copies, is worth real money.
About 52 API servers at peak, if each handles 2,000 metadata requests per second at around half its CPU. The per-server number is itself an assumption; measure it with a load test rather than trusting a figure from the internet. Add headroom for a zone failure: with three availability zones, losing one leaves two-thirds of the fleet, so you’d run roughly 78.
How wrong could it be?
A single estimate hides its uncertainty. Better practice is to show a range by changing the shakiest assumptions together:
sensitivity of yearly raw blob storage (PB):
low uploads/user 0.1 MB/upload 1.5 -> 8 PB
likely uploads/user 0.2 MB/upload 2.8 -> 31 PB
high uploads/user 0.4 MB/upload 5.0 -> 110 PBThe range is more than tenfold. That’s normal, and saying so is a strength. It tells you which assumptions to validate first (how often people post, and how big the stored files really are) and that the storage design must scale smoothly across that range rather than be tuned to one number.
Mistakes interviewers listen for
Using the daily average as the design load. Systems fail at the peak. 35,000 views per second on average and 104,000 at the busiest hour are different systems. If the product has spiky events (a sports final, a ticket release), the peak factor can be 10x or more.
Mixing bits and bytes. 5.2 GB/s of image data is about 42 Gbit/s on the wire. Getting this wrong understates network needs by 8x.
Forgetting copies. Replicas, derived image sizes, backups and indexes each multiply raw size. “10 PB a year” is the logical number; the bill is for 31.
False precision. Quoting “104,167 requests per second” implies a certainty you don’t have. Say “about 100,000” and move on.
Estimating without a purpose. If the numbers don’t change any decision, you’ve spent five minutes of a 45-minute interview on arithmetic. Estimate what’s in question (storage tiering, sharding, CDN) and skip the rest.
Confusing DAU and MAU. Monthly actives can be three to five times daily actives. Use the one that matches the rate you’re computing.
Interview practice: storage for a chat service
“Estimate message throughput and five-year storage for a chat app with 20 million daily active users.”
Answer
I’d start by stating assumptions: each active user sends 40 messages a day; each stored message is about 300 bytes once you count the text, sender and conversation ids, a timestamp and index overhead; the busiest hour runs at 4x the average because chat is concentrated in evenings; and we keep three copies.
That’s 800 million messages a day, about 9,300 per second on average and around 37,000 per second at peak. Storage is 240 GB a day, roughly 438 TB over five years before replication and about 1.3 PB with three copies.
What that tells me: 37,000 writes per second at peak is too much for a single relational primary to absorb comfortably, so the message store should be partitioned, most naturally by conversation id, which keeps a conversation’s messages together for the “load recent messages” query. 1.3 PB argues for tiering old messages to cheaper storage, since people rarely scroll back years. I’d also flag the assumption I trust least, the 40 messages per user per day, because group chats with heavy users could easily double it, and say the partitioning scheme should absorb that without redesign.