Back-of-the-Envelope Estimation

August 28, 202615 min readUpdated 8/22/2026

“We’ll need a few servers and probably a cache.” That sentence is why estimation is on the scorecard. It sounds like an answer and contains no information — it would be equally true of a system with a hundred users or a hundred million, and those are not the same system.

Estimation is the five minutes that turn a vague problem into a specific one. It is also the step candidates most often skip, and interviewers notice, because it is the cheapest possible signal that someone has operated a real system rather than read about one.

What it is for

Not accuracy. Nobody is checking your multiplication. It is for making the next twenty minutes decidable.

Consider two answers to “should we cache this?”

  "Caching would probably help here."

  "40,000 reads/sec against 300 writes/sec — 130:1. The read path IS the
   problem, and at that ratio a 5-minute TTL is ~99% hit rate. Cache it."

The second one has already made three decisions and justified them. It also produced a number that can be wrong, which is the point: a claim you can check beats a claim you cannot.

Estimation also catches the class of error that matters. Being out by a factor of three changes nothing — you would build the same system. Being out by a factor of a thousand means you are designing something else entirely, and the arithmetic is how you notice.

The numbers worth memorising

You need very few. These are the ones that come up constantly.

Powers of two, as data sizes

   2^10  =  1 thousand    1 KB
   2^20  =  1 million     1 MB
   2^30  =  1 billion     1 GB
   2^40  =  1 trillion    1 TB
   2^50  =                1 PB

And a few typical sizes, because you will be asked how big a record is:

   a char / ASCII byte        1 B
   an integer, a timestamp    4-8 B
   a UUID                     16 B binary, 36 B as text
   a short URL row            ~500 B      (id, long url, owner, dates)
   a tweet row                ~300 B      (280 chars + metadata)
   a compressed photo         ~200 KB
   a minute of 1080p video    ~50 MB

Latency, ordered

The exact figures drift with hardware; the ratios do not, and the ratios are what the argument rests on.

OperationRoughlyRelative
L1 cache reference1 ns1
Main memory reference100 ns100
Read 1 MB from memory~10 µs10,000
SSD random read~100 µs100,000
Read 1 MB from SSD~200 µs200,000
Round trip within a datacenter~500 µs500,000
Disk seek (spinning)~10 ms10,000,000
Round trip California → Netherlands~150 ms150,000,000

Three conclusions fall straight out of that table, and they justify most of the caching and CDN decisions in this track:

  • Memory is roughly a thousand times faster than SSD. That gap is the entire argument for a cache.
  • A datacenter round trip costs more than reading a megabyte from memory. So the number of calls usually matters more than the size of each one — which is why N+1 queries are so damaging and why batching helps so much.
  • Crossing an ocean costs 150ms and physics is not negotiable. No amount of server tuning fixes it. Only moving the data closer does, which is what a CDN is.

Time, so per-day converts to per-second

   1 day   = 86,400 seconds   ≈ 100,000    <- use 100k, always
   1 month = ~2.6 million seconds
   1 year  = ~31.5 million seconds

Rounding 86,400 to 100,000 is the single most useful shortcut in estimation. Dividing by 100,000 is moving a decimal point, and the 16% error it introduces is far below the noise in every other number you are using.

QPS, and the trap in the word

“Queries per second” gets used for three different things and the difference matters.

Requests per second is what arrives at your load balancer. Queries per second is what arrives at your database — and one request is rarely one query. A listing page that loads the property, its images, its amenities and its host is four queries, so 100 requests a second is 400 queries a second unless something joins or caches them.

   300 page views/sec
      x 4 queries each (property, images, amenities, host)
      = 1,200 queries/sec        <- what the database actually sees

   with a cache at 90% hit rate
      = 120 queries/sec          <- what it sees after

That multiplier is where N+1 query bugs turn into outages: a page that issues one query per result in a list of fifty is not 4x, it is 50x, and it looks completely fine in development against twelve rows. Working out the fan-out explicitly is worth the ten seconds.

Transactions per second is the third, and it is the one that has a hard ceiling, because a write transaction has to be durably committed to disk. A single Postgres instance manages thousands of small write transactions a second, not hundreds of thousands.

The five-step recipe

   1.  users        daily active users
   2.  x actions    per user, per day        -> writes/day
   3.  / 100k       seconds in a day         -> writes/sec
   4.  x ratio      read:write               -> reads/sec
   5.  x size       bytes per record         -> storage/day, /year
                    x QPS                    -> bandwidth

Then one more, which people forget: peak. Traffic is not flat. A rule of thumb is peak is two to three times average, so multiply and design for that. A system sized for its average falls over every evening.

Worked: a URL shortener

State the assumptions out loud. Anyone can disagree with an assumption; nobody can disagree with a number you refuse to give.

ASSUME
   100M new URLs per day
   10:1 reads to writes
   ~500 bytes per row
   links kept 5 years

WRITES
   100M / 100k          = 1,000 writes/sec
   peak x3              = 3,000 writes/sec

READS
   1,000 x 10           = 10,000 reads/sec
   peak x3              = 30,000 reads/sec

STORAGE
   100M x 500 B         = 50 GB/day
   x 365 x 5            = ~91 TB over 5 years

BANDWIDTH
   write 50 GB/day / 100k s   = ~0.5 MB/s in
   read  10k/s x 500 B        = ~5 MB/s out

Now read what those numbers actually said.

3,000 writes a second is fine. One well-indexed Postgres instance does that without complaint. No sharding, no queue in front of it, nothing exotic. Say so — declining to over-engineer is a positive signal.

30,000 reads a second is not fine for the same instance — but redirects are the most cacheable thing imaginable. They are immutable lookups by key. A cache absorbs essentially all of it and the database sees the misses only.

91 TB is the interesting number. It is too much for one machine, so something must give: expire old links, tier cold data to object storage, or shard. That is a design conversation the arithmetic just handed you, and you would not have found it by drawing boxes.

5 MB/s is nothing. Bandwidth is not a constraint here. Saying that explicitly shows you checked rather than assumed.

Where those numbers came from

Two of the four assumptions above are worth defending, because “100 million a day” sounds like it was invented and largely was.

The row size is not a guess, though — you can add it up. A shortener row holds a short code, the original URL, an owner, a creation timestamp and an expiry:

CREATE TABLE urls (
    short_code  VARCHAR(7)   PRIMARY KEY,   --   7 B
    long_url    VARCHAR(2048) NOT NULL,     -- ~100 B typical, 2 KB worst case
    user_id     BIGINT,                     --   8 B
    created_at  TIMESTAMPTZ NOT NULL,       --   8 B
    expires_at  TIMESTAMPTZ                 --   8 B
);                                          -- ~130 B + row overhead + index

That is closer to 130 bytes than 500. Rounding up to 500 covers Postgres’s per-row overhead, the primary key index, and the occasional enormous URL — and rounding up on storage is the right direction to be wrong in. Say that out loud too: “call it 500 bytes with indexes and overhead” is a much stronger line than an unexplained 500.

The volume, by contrast, is genuinely arbitrary, and the correct move is to label it as such. “I’ll assume 100 million a day — tell me if you had a different scale in mind” takes three seconds and either gets you the real number or an agreement to proceed. Both outcomes are better than a silent guess.

Sizing the cache

“Add a cache” is not a design until you say how big. The usual starting point is the 80/20 rule — 20% of the keys serve 80% of the traffic — and for URL shorteners the skew is far more extreme than that.

   daily reads          10k/s x 86,400   ≈ 864M reads/day
   distinct URLs hit    assume 20% of the day's 100M    = 20M
   x 500 bytes                                          = 10 GB

   10 GB fits in memory on one machine, comfortably.

Ten gigabytes is an ordinary Redis instance. If the arithmetic had produced ten terabytes, the answer would be different — a distributed cache, or caching only the hot tail — and again, the number is what tells you which conversation to have.

Worked: a booking system

The second example is deliberately smaller, because most systems are, and because the interesting result is the opposite one.

StayHub’s development database holds 12 listings and 4 users, which is a seed fixture rather than a business. So scale it to something plausible: a regional booking site with 2 million listings and 500,000 daily active users.

ASSUME
   500k daily active users
   each views ~20 listings, searches ~5 times
   1 in 50 visits ends in a booking
   listing row + images metadata ~2 KB

READS
   500k x 20 / 100k     = 100 listing views/sec
   500k x 5  / 100k     =  25 searches/sec
   peak x3              = 300 views/sec, 75 searches/sec

WRITES
   500k / 50 = 10k bookings/day / 100k  = 0.1 bookings/sec
   peak x3                              = 0.3 bookings/sec

STORAGE
   2M listings x 2 KB   = 4 GB          (all of it fits in memory)
   10k bookings/day x 1 KB x 365        = ~3.6 GB/year

Now the conclusions, and they are almost the reverse of the shortener’s.

Bookings are 0.3 per second at peak. That is not a scale problem in any sense. Nothing about the write path needs sharding, queuing or partitioning — and yet the booking path is where all the engineering goes, because correctness is the problem, not throughput. Two people booking the same room at the same instant is a disaster at 0.3 writes a second exactly as much as at 3,000.

This is the most useful thing estimation does, and it gets underrated: it tells you when the hard part is not scale. A candidate who computes 0.3 writes a second and then spends fifteen minutes sharding the bookings table has been given the answer and not read it.

Search at 75/sec against 2 million listings is the read problem, and it is not one SQL can do well — text matching plus a dozen optional filters. That is what justifies a search index, and the arithmetic is what justifies it rather than habit.

4 GB of listings fits in memory. The entire catalog can be cached. That is a genuinely different design from one where it cannot, and you only know which you are in by multiplying.

How many servers?

The question that follows every QPS figure, and the one people guess at. It is a division, and the only hard part is the number you divide by.

A single application server handles somewhere between a few hundred and a few thousand requests per second, and where in that range depends almost entirely on what a request does:

Request doesRoughly per server
Serves from memory, no I/O5,000–20,000/sec
One cached read2,000–5,000/sec
One or two indexed database queries500–2,000/sec
Several queries, or a call to another service100–500/sec
Anything CPU-bound — image work, PDF, crypto10–100/sec

So for the shortener’s peak of 30,000 reads a second, where a read is one cache hit:

   30,000 / 3,000 per server   = 10 servers
   + headroom to lose some     = 13-15
   spread across 3 zones       = 5 per zone

The headroom is the part worth saying out loud. Running exactly ten means losing one puts the other nine over capacity, and they then fall over in sequence — which is how a single instance failure becomes a total outage. Size for N+2, or for losing a whole availability zone, and say which you chose.

Storage is bigger than you calculated

Raw row size times row count is the beginning of the answer, not the end. Real storage carries several multipliers, and forgetting them is how a “91 TB” estimate becomes a 300 TB invoice.

   raw data                     91 TB
   x indexes           +30%     118 TB     an index is a copy of its columns
   x replication       x3       354 TB     three copies is the normal default
   + backups           +1 copy  ~450 TB    and they are retained for weeks
   + free headroom     +30%     ~580 TB    a full disk is an outage

The replication factor is the one that dominates, and it is not optional — a single copy of your data is not durable. Three is the usual answer, and it means the honest number is roughly five times the raw figure, not the raw figure.

Nobody expects you to produce all five lines. Saying “call it 91 TB raw, so realistically three to five times that with replication and indexes” is exactly the right amount of detail.

Worked: a chat system

One more, because it produces a different bottleneck again — and one that is not a database at all.

ASSUME
   50M daily active users
   each sends 40 messages/day
   messages are ~100 bytes of text
   users are connected ~5 hours/day

MESSAGES
   50M x 40             = 2B messages/day
   2B / 100k            = 20,000 messages/sec
   peak x3              = 60,000 messages/sec

STORAGE
   2B x 100 B           = 200 GB/day
   x 365                = 73 TB/year   (x3 replication -> ~220 TB)

CONNECTIONS  <- the number that actually matters
   50M x (5/24)         = ~10M concurrent connections
   at ~10k per server   = 1,000 connection servers

The message rate is high but not remarkable — 60,000 writes a second is a sharding problem with a well-understood answer. The number that shapes this design is ten million concurrent connections.

Chat cannot be request/response, because the server has to push. So every online user holds an open WebSocket, each one consuming a file descriptor and a few kilobytes of kernel memory, and a tuned server handles perhaps ten thousand of them. A thousand machines exist purely to hold connections open, doing almost nothing.

That is why chat designs have a component no other system has: a registry mapping user → which connection server currently holds them, so a message from a user on server 400 can reach a user on server 812. The estimate is what makes that component obviously necessary rather than a detail someone remembered.

When a number tells you the design is wrong

Certain results should stop you.

If you compute…It probably means…
> 10k writes/sec to one tableOne primary will not do it — partition, shard, or batch
> 100k reads/secThe design is a caching design; everything else is secondary
Petabytes within a yearObject storage and tiering, not a database
> 10 Gbps egressA CDN is mandatory, not an optimisation
A cache larger than a big machine’s RAMDistributed cache, or cache less
Under 100 requests/secOne server and a database. Say so and move on.

That last row is the one people are least comfortable saying and it is often the correct answer.

Ratios worth carrying in your head

Most estimates begin with a guess at a ratio, and being roughly right about these saves the whole exercise from being arbitrary.

SystemRead:writeWhy
Social feed100:1Everyone reads constantly, few post
URL shortener10:1 to 100:1One create, many redirects
E-commerce50:1Long browsing, rare purchase
Booking site200:1Heavy comparison, one booking
Chat~1:1Every message written is read once or twice
Analytics ingestion1:100 (write-heavy)Everything is written, little is queried

The two extremes at the bottom are the useful ones. Chat and analytics are the systems where “add a cache and read replicas” is not the answer, because the load is not reads — and recognising that early saves you from designing the wrong system politely.

Two more conversion rates that come up whenever a funnel is involved: roughly 10% of registered users are active on a given day, and roughly 1–3% of visits to a commerce or booking site end in a transaction. Both are order-of-magnitude figures, and both are worth stating as assumptions so they can be corrected.

What to do when you have no idea

Sometimes you genuinely cannot guess the top-line number — how many people use a service you have never seen. Work from something you can anchor to instead.

   world population              ~8 billion
   internet users                ~5 billion
   a very large social network   ~2 billion MAU,  ~500M DAU
   a large national service      ~50M users
   a successful startup          ~1M users
   a B2B product                 ~10k accounts

Then place the thing you are designing between two of those and say why. “This is a national booking service, so somewhere below a large social network and well above a startup — call it 20 million monthly, 2 million daily” is a defensible number, arrived at in ten seconds, and everyone in the room now agrees what is being designed.

The alternative — refusing to pick because you are not sure — leaves the rest of the hour with nothing to stand on.

How to do it out loud

The arithmetic is the easy half. Presenting it is where it goes wrong.

  • Say the assumption before the number. “Let’s assume 100 million a day” invites a correction, which is useful. A number with no assumption attached cannot be corrected, only doubted.
  • Round hard, and say you are. 86,400 becomes 100,000. 1.7 becomes 2. Nobody wants to watch long division.
  • Keep the units visible. Writing 100M/day ÷ 100k s/day = 1k/s makes an error obvious. Most estimation mistakes are unit slips, not arithmetic.
  • Interpret every result. The number is not the deliverable; the sentence after it is. “91 TB, so this cannot live on one machine for five years” is what you are being paid for.
  • Do not compute what you will not use. If bandwidth is obviously irrelevant, say it is obviously irrelevant and skip it.

A worked sanity check

One last technique, and it is the one that catches genuine blunders: estimate the same quantity a second way and see whether the two agree.

The shortener estimate said 91 TB over five years. Check it from the other end — how many rows is that, and is that a plausible number of URLs?

   100M/day x 365 x 5   = 182 billion rows

   A 7-character base62 code has 62^7 ≈ 3.5 trillion possibilities.
   182 billion / 3.5 trillion = ~5% of the keyspace used.

Those two agree, and they also just answered a question you had not asked: seven characters is enough. At 5% occupancy, random code generation still collides rarely, so a generate-and-retry scheme terminates quickly. Six characters would be 56 billion possibilities against 182 billion rows — impossible — and the arithmetic says so in one line.

That is the estimation step earning its keep twice: it sized the storage, and then it settled a design decision that would otherwise have been a preference.

The thing to actually remember

Three shortcuts carry most of the load, and they are worth having automatic:

   per day -> per second     divide by 100,000
   peak                      multiply average by 3
   memory vs disk            ~1000x — this is why caches exist

Everything else is one multiplication at a time, out loud, with the units written down.

And one habit: after every number, say the sentence it implies. “30,000 reads a second, so the read path is the design.” “0.3 writes a second, so throughput is not the problem here — correctness is.” The arithmetic is a means; the sentence is the output.

Next: load balancing and the stateless tier, which is what you build once the arithmetic says one server is not enough.