Estimation: numbers that pick the design
Turn users into requests a second, bytes and bits. Then compare each number with what one node handles. Five minutes of arithmetic tells you which part of the design needs the most attention.
- 1A day has 86,400 seconds. Call it 10⁵: 1 million a day is about 10 a second.
- 2State a peak factor. Size for the peak, not the average.
- 3Storage is writes a day × bytes × days kept, for one copy. Then multiply by the copies.
- 4Compare each number with one node. The first number over the line picks the design.
- Write each step on the board. The interviewer checks the method, not the digits.
- Split reads from writes early. They scale in different ways.
- Storage counts one copy. Say the replica count as a separate multiplier.
Ten million users at 15 requests each is 150 million a day. That is about 1,700 a second on average and 17,000 at a 10× peak.
| power | count | bytes |
|---|---|---|
| 10³ | thousand | KB |
| 10⁶ | million | MB |
| 10⁹ | billion | GB |
| 10¹² | trillion | TB |
| 10¹⁵ | quadrillion | PB |
| shortcut | arithmetic |
|---|---|
| 1 day ≈ 10⁵ s | 24 × 3,600 = 86,400 |
| 1 month ≈ 2.6 M s | 30 × 86,400 = 2,592,000 |
| 1 year ≈ 31.5 M s | 365 × 86,400 = 31,536,000 |
| 1 M a day ≈ 12/s | 10⁶ ÷ 86,400 = 11.6 |
| 1 B a day ≈ 12,000/s | 10⁹ ÷ 86,400 = 11,574 |
| 2¹⁰ ≈ 10³ | 1,024 |
I round to powers of ten and say the rounding out loud.
Grey rows: Peter Norvig's published timing table, quoted as orders of magnitude. Coloured rows: measured in the lab, one client on the same laptop, median of 2,000 calls.
Memory is 100 nanoseconds, a call on the same network is about 100 microseconds, and a round trip across an ocean is 150 milliseconds.
| where | operation | measured | plan with |
|---|---|---|---|
| Postgres | Primary-key read | 104,800/s | 100,000/s |
| Postgres | One-row insert, committed | 36,465/s | 10,000/s |
| Postgres | Booking confirm, spread (T8) | 19,500/s | 10,000/s |
| Postgres | Booking confirm, one hot row (T8) | 2,500/s | 1,000/s |
| Redis | GET | 65,900/s | 10,000/s |
| Redis | SET | 57,400/s | 10,000/s |
| Redis | 3-night hold script (T8) | 55,000/s | 10,000/s |
| Postgres | Storage, one managed instance | 64 TiB, cited | 10 TB |
Postgres 16 and Redis 8 on a 16-thread laptop, 32 clients, 3 seconds per run. Other test runs shared the machine. The storage ceiling is from the Amazon RDS documentation.
One Postgres node does about 10⁵ key reads and 10⁴ committed writes a second. I plan with the lower power of ten.
150 M requests a day÷ 86,400 s1.74 k/s average× 1017.4 k/s peak
| per second | average | peak |
|---|---|---|
| requests | 1.74 k | 17.4 k |
| writes | 17.4 | 174 |
| reads | 1.72 k | 17.2 k |
- storage
- 1.5 GB a day, 2.74 TB in 5 yr (one copy)
- bandwidth
- 1.39 Mbit/s in, 138 Mbit/s out, at peak
- 1One primaryplus a standby
- 2+ replicas, cachereads spread out
- 3+ shardswrites and bytes split
Start at step 1. One primary carries the writes, the reads and the bytes. Add a standby for failover.
Peak writes, peak reads and bytes each have a line for one node. Whichever crosses its line first decides my first design step.
estimate(users, per_user, reads_per_write, bytes, years, peak):
per_day = users × per_user // requests a day
writes = per_day / (1 + reads_per_write1)
reads = per_day - writes
avg = per_day / 86,4002 // seconds in a day
peak_qps = avg × peak3
storage = writes × bytes × 365 × years // one copy4
egress = peak reads a second × bytes × 85 // bits a second
read_nodes = ceil(peak reads / 100,0006)
shards = max(ceil(peak writes / 10,000), ceil(storage / 10 TB7))
IF shards > 1: step = 3 // shard by key
ELSE IF read_nodes > 1: step = 2 // replicas and a cache
ELSE: step = 1 // one primary- 1A ratio of 99 reads per write means 1 request in 100 is a write.
- 2Seconds in a day. Round it to 10⁵ when you work in your head.
- 3The peak factor is an assumption. Say it out loud: 2× for steady traffic, 10× for a flash sale.
- 4Replicas, indexes and backups multiply this. Name the multiplier.
- 5Links are rated in bits a second. Bytes a second × 8.
- 6Measured Postgres key reads, rounded down to a power of ten.
- 764 TiB is the largest managed instance. Rounded down, it is 10 TB.
Tested source Go: the estimator
// Estimate turns daily users into the numbers of the round.
func Estimate(in Input, lim Limits) Output {
var o Output
o.RequestsPerDay = in.DailyUsers * in.ActionsPerUser
o.WritesPerDay = o.RequestsPerDay / (1 + in.ReadsPerWrite)
o.ReadsPerDay = o.RequestsPerDay - o.WritesPerDay
o.AvgQPS = o.RequestsPerDay / SecondsPerDay
o.PeakQPS = o.AvgQPS * in.PeakFactor
o.AvgWriteQPS = o.WritesPerDay / SecondsPerDay
o.AvgReadQPS = o.ReadsPerDay / SecondsPerDay
o.PeakWriteQPS = o.AvgWriteQPS * in.PeakFactor
o.PeakReadQPS = o.AvgReadQPS * in.PeakFactor
o.BytesPerDay = o.WritesPerDay * in.ObjectBytes
o.BytesTotal = o.BytesPerDay * DaysPerYear * in.RetentionYears
o.IngressBitsSec = o.PeakWriteQPS * in.ObjectBytes * 8
o.EgressBitsSec = o.PeakReadQPS * in.ObjectBytes * 8
o.ReadNodes = atLeastOne(o.PeakReadQPS / lim.ReadsPerNode)
o.WriteShards = atLeastOne(o.PeakWriteQPS / lim.WritesPerPrimary)
o.ByteShards = atLeastOne(o.BytesTotal / lim.BytesPerNode)
o.Shards = max(o.WriteShards, o.ByteShards)
switch {
case o.Shards > 1:
o.Step = 3 // shard the primary
case o.ReadNodes > 1:
o.Step = 2 // add read replicas and a cache
default:
o.Step = 1 // one primary and a standby
}
return o
}
func atLeastOne(x float64) int {
return max(1, int(math.Ceil(x)))
}
Every number on my board comes from one of these lines, so I can show the interviewer where it came from.
| preset | peak w/s | peak r/s | bytes | step |
|---|---|---|---|---|
| Booking | 174 | 17.2 k | 2.74 TB | 1 |
| News feed | 2.86 k | 286 k | 1.81 TB | 2 |
| URL shortener | 3.47 k | 34.7 k | 183 TB | 3 |
| Chat | 46.3 k | 46.3 k | 146 TB | 3 |
| preset | read nodes | by writes | by bytes |
|---|---|---|---|
| Booking | 1 | 1 | 1 |
| News feed | 3 | 1 | 1 |
| URL shortener | 1 | 1 | 19 |
| Chat | 1 | 5 | 15 |
Nodes each number needs. The largest shard count wins. Inputs for each preset:
- booking
- 10 M users × 15, 99 : 1 reads, 1 kB, 5 yr, 10× peak
- news feed
- 50 M users × 100, 100 : 1 reads, 100 B, 1 yr, 5× peak
- url shortener
- 100 M users × 11, 10 : 1 reads, 500 B, 10 yr, 3× peak
- chat
- 50 M users × 80, 1 : 1 reads, 200 B, 1 yr, 2× peak
The URL shortener is small in requests but large in bytes. Ten years of links decide its shards.
| habit | status | why |
|---|---|---|
| Round to one significant figure and powers of ten | Approved | Fast, and errors of 20% do not change the design. |
| Say the peak factor and why you chose it | Approved | The interviewer can push back on one number, not on the method. |
| Split reads from writes | Approved | Reads scale with copies and caches. Writes scale only with shards. |
| Size the servers for the average | Not approved | The system fails at the peak, when the most users are watching. |
| Long division to three decimals | Not approved | Uses minutes and changes no decision. |
| Estimate every component before the design | Not approved | Estimate only the numbers that pick a component: peak QPS, bytes, bandwidth. |
| Storage without replicas or indexes | Name the multiplier | One copy is the honest base. Then say "× 3 for replicas". |
| Skip the numbers | Only if asked | Some interviewers say scale is small. Otherwise the numbers are the reason for each box. |
I give the number, the arithmetic behind it, and the decision it changes.
| step | add | it handles | move up when you see |
|---|---|---|---|
| 1 | One Postgres primary and a standby for failover. Stateless services behind a load balancer. | Up to about 10,000 writes and 100,000 key reads a second, and 10 TB of data. | Peak reads pass one node. |
| 2 | Read replicas and a cache. Reads go to copies; writes stay on the primary. | Reads grow with copies. Cache hits never reach Postgres. | Peak writes pass one primary, or the data passes one node. |
| 3 | Shards by key. Each shard is a primary with its own copies. | Each shard takes about 10,000 writes a second and 10 TB. Add shards as you grow. | One key gets more traffic than one shard takes: a hot key. Split it or queue it. |
Capacity measured on one laptop. Read it as orders of magnitude. A server with durable storage commits slower, and a larger server reads faster.
My numbers put this design at step 1. I would add replicas when peak reads pass about 100,000 a second, and shard when writes pass 10,000.
0 of 8 known
A service gets 1 million requests a day. About how many is that a second?
The average is 1,000 requests a second and the peak factor is 3. What do you size the servers for?
100 million new links a day, 500 bytes each, kept 10 years. How much storage?
Reads outnumber writes 100 to 1. Which path do you optimize first?
The chat preset peaks at 46,296 writes a second. One primary takes about 10,000. What do you say?
The lab measured 36,465 committed inserts a second. Why does the estimator plan with 10,000?
A local Redis round trip takes about 150 µs. How many sequential calls fit in a 100 ms budget?
17,000 reads a second return 1 KB each. What is the bandwidth out?
- a day
- 86,400 seconds, about 10⁵.
- 1 M a day
- About 12 a second.
- a year
- About 31.5 million seconds.
- memory
- About 100 ns a read.
- local call
- About 50 to 150 µs, measured.
- ocean
- About 150 ms there and back.
- Postgres
- About 10⁵ key reads and 10⁴ committed writes a second, measured.
- Redis
- About 10⁴ to 10⁵ simple commands a second, one round trip each.
- one node
- 64 TiB is the largest managed Postgres instance.
Lab numbers: Postgres 16 and Redis 8 on an 8-core, 16-thread laptop, 32 clients. Use them as orders of magnitude.