Back-of-envelope capacity math: a worked search-service sizing

What it is

The practice of deriving a system's shape from arithmetic in a few minutes, before drawing any boxes. It is not estimation for its own sake: the numbers decide the architecture, and a design produced without them is a design produced from familiarity.

The discipline has three parts:

1. STATE ASSUMPTIONS EXPLICITLY, so they can be corrected.
   "Assume 50 million monthly actives" is a number the
   interviewer can revise; "assume high traffic" is not.

2. WORK IN ROUND NUMBERS. 100,000 seconds per day, not 86,400.
   The answer needs to be right to within a factor of two, and
   precision beyond that is wasted time and invites arithmetic
   errors.

3. DERIVE THE ARCHITECTURE FROM THE RESULT, not before it.
   The sentence that matters is "X is Y, therefore Z", and a
   candidate who computes numbers and then designs something
   unrelated to them has done the arithmetic as decoration.

Commonly confused with capacity planning, which is a production activity with real measurements. This is design-time sizing: the goal is to establish which constraint binds, not to size a purchase order.

The problem it solves

Without the arithmetic, every design decision is made from familiarity rather than from constraint, and the failure mode is specific: a system designed for the wrong bottleneck.

Design instinct               What the arithmetic says
-----------------------------------------------------------------
"We need a distributed        1 million jobs/day is 12/sec and
 database"                    10 GB. That is one Postgres.

"Vector search needs a        2 million vectors x 256 fp16 is
 dedicated cluster"           1 GB. It fits in every process.

"We should cache this"        The cache round trip is 500 µs and
                              the query is 800 µs. Not worth it.

"Just add more shards"        The index is 40 GB. Twelve shards
                              means 3 GB each and the query cost
                              is dominated by scatter-gather.

Each of those is a real design conversation settled in thirty seconds by arithmetic.

Mechanics: the numbers to have

TIME
  Seconds per day             ~100,000  (86,400, rounded)
  Seconds per month           ~2,500,000
  Seconds per year            ~30,000,000

SIZES
  ASCII character                 1 byte
  UUID                           16 bytes (36 as a string)
  Timestamp                       8 bytes
  A typical JSON record       200 bytes to 2 KB
  A web page                  1 to 3 MB
  A photo                     200 KB to 5 MB
  An embedding (768 fp32)      3 KB   (768 int8: 768 bytes)

THROUGHPUT (single node, order of magnitude)
  Postgres simple reads        10,000 to 50,000/sec
  Postgres writes               5,000 to 20,000/sec
  Redis                       100,000/sec (pipelined: 1M/sec)
  Kafka per broker            100 MB/sec sustained
  HTTP service (simple)        5,000 to 20,000 req/sec/core-ish
  NVMe                        500,000 IOPS, 3 GB/sec

RATIOS
  Peak to average              2x to 5x   (10x for events)
  Read to write                10:1 to 1000:1 for user-facing
  Compression                  3:1 to 10:1 for text

The two formulas that turn traffic into fleet size:

LITTLE'S LAW          L = λ × W
                      concurrency = arrival rate × service time
                      This sizes thread pools, connection pools
                      and instance counts.

UTILISATION           At utilisation ρ, queueing time is
                      W_q = W_service × ρ / (1 - ρ)
                      At ρ = 0.8, waiting is 4x service time.
                      At ρ = 0.9, it is 9x.
                      *** Never size for above 70% utilisation. ***

See Little's Law and queueing for the derivation.

The worked sizing: a product search service

The problem statement: search over a 50-million-item catalogue for a marketplace with 20 million monthly active users. p99 under 200 ms.

Step 1: traffic

ASSUMPTIONS (state them, invite correction)
  20M monthly active users
  Each active user searches ~5 times per active day
  Users are active ~10 days per month
  -> 20M x 5 x 10 = 1 billion searches/month

AVERAGE QPS
  1e9 / 2.5e6 seconds = 400 QPS

PEAK
  Marketplace traffic is diurnal with a 3x peak, and a seasonal
  peak (Black Friday) at 4x the normal peak.
  -> Design for 400 x 3 = 1,200 QPS normal peak
  -> Headroom for 400 x 12 = 4,800 QPS seasonal

The first architectural consequence: 1,200 QPS is not a large number. This tells us immediately that the design is not about throughput, and a candidate who starts sharding for QPS at this point has not done the arithmetic.

Step 2: storage

CATALOGUE
  50M items x ~2 KB of source data = 100 GB

INVERTED INDEX
  Rule of thumb: a Lucene index is 0.3x to 1x the source text
  depending on stored fields and doc values.
  Assume 0.5x on the indexed text (say 60% of the source):
  50M x 2 KB x 0.6 x 0.5 = 30 GB

VECTORS (for hybrid retrieval)
  50M x 768 dims x 4 bytes (fp32)         = 154 GB
  50M x 768 dims x 1 byte  (int8)         =  38 GB
  + HNSW graph at M=32: 50M x ~280 bytes  =  14 GB
  -> 52 GB quantised, 168 GB unquantised

TOTAL SERVING FOOTPRINT
  30 GB lexical + 52 GB vector = 82 GB

The second architectural consequence, and the one that decides the design: 82 GB fits comfortably on a single 128 GB instance. This is not a distributed search problem. It is a replicated-single-node problem, which is a completely different and much simpler system.

And the counterfactual that shows the arithmetic mattered: at fp32 the vectors alone are 154 GB and the total is 184 GB, which does not fit and forces sharding. The quantisation decision is what determines whether this is a sharded system, and that is worth saying out loud because it reverses the usual order: normally you shard and then optimise, and here the optimisation removes the need to shard.

Step 3: fleet size, from Little's Law

SERVICE TIME per search (budget, from the funnel)
  lexical retrieval        15 ms
  vector retrieval         25 ms   (parallel with lexical)
  fusion + filter           3 ms
  rerank (top 50)          35 ms
  serialise                 2 ms
  -> ~65 ms of work, with retrieval overlapped

CONCURRENCY at peak
  L = λ × W = 1,200 QPS × 0.065 s = 78 concurrent requests

THREADS AND UTILISATION
  Never size for above 70%: 78 / 0.7 = 112 concurrent slots.

PER NODE
  A 16-core node with the index in memory handles roughly
  16 / 0.065 = 246 requests/sec at 100% CPU,
  so ~170/sec at 70% utilisation.

FLEET
  1,200 / 170 = 7 nodes for the normal peak.
  Round to 9 for AZ balance (3 per AZ across 3 AZs).
  For the seasonal 4,800 QPS peak: 28 nodes, so autoscale
  3x-4x or over-provision seasonally.

The third consequence: nine nodes, each holding the entire 82 GB index. Every node can answer every query with no scatter-gather, which removes the tail-at-scale problem entirely. That is a substantial simplification and it fell out of the storage arithmetic rather than from a preference.

Step 4: the write path

CATALOGUE CHANGES
  Price and stock: assume 4% of items change per day
    50M x 0.04 = 2M changes/day = 23/sec average, 100/sec peak
  Text: assume 0.2% per day
    50M x 0.002 = 100k/day = ~1/sec

EMBEDDING COST
  Only text changes need re-embedding: 100k/day.
  At 2,000 embeddings/sec on one GPU: 50 seconds of GPU per day.
  *** Negligible. ***

  If we re-embedded on EVERY change including price:
  2M/day at 2,000/sec = 1,000 seconds/day, still not huge,
  but it would churn the HNSW graph continuously and force
  constant rebuilds.
  -> Separate the paths: price/stock to an attribute store
     applied at ranking time; text through the embedding
     pipeline.

INDEX REBUILD
  HNSW build for 50M vectors at M=32, efConstruction=200:
  roughly 4-8 hours on 32 cores.
  -> Weekly offline rebuild, alias swap. Not an online operation.

The fourth consequence: the freshness split is forced by the rebuild cost, not chosen for elegance. Two million daily price changes cannot go through a pipeline whose rebuild takes six hours.

Step 5: check the assumptions that would change the answer

This step is what separates a good sizing from a recited one. Which assumption, if wrong by a factor of three, changes the architecture?

ASSUMPTION                  If 3x wrong, does the design change?
------------------------------------------------------------------
20M MAU                     No. 3,600 QPS still fits 25 nodes,
                            still no sharding.
50M items                   YES. 150M items x int8 = 156 GB,
                            which does not fit one node. This is
                            the assumption that matters.
2 KB per item               YES, for the same reason via index size.
65 ms service time          No. 195 ms would break the p99 budget
                            long before it broke the fleet size,
                            so it is a latency problem not a
                            capacity one.
4% daily price change       No. Even 12% is 6M/day = 70/sec.

"The catalogue size is the assumption that decides whether this is sharded" is the sentence to say, because it tells the interviewer exactly what to probe and demonstrates you know which number is load-bearing.

The summary that falls out

ARCHITECTURE, derived rather than chosen:

  9 nodes, 3 per AZ, each holding the full 82 GB index
  int8-quantised vectors (this is what avoids sharding)
  No scatter-gather, so no tail-at-scale problem
  Autoscale to ~28 for seasonal peaks
  Two write paths: attributes (seconds) and text (hourly)
  Weekly offline HNSW rebuild with an alias swap
  A read-through cache for head queries, since the top 1% of
    queries are typically 30-50% of traffic

Production evidence

Jeff Dean's "Numbers Everyone Should Know" and his Stanford lecture on building large-scale systems established this practice as an explicit interview and design skill at Google, and the framing that the numbers should determine the design is his.

Little's Law (Little, 1961) is the theorem behind the concurrency arithmetic, and its generality (it holds for any stable queueing system regardless of arrival or service distribution) is why it can be applied without knowing the distributions.

The Universal Scalability Law (Gunther) supplies the utilisation caution: contention and coherence terms mean that throughput does not scale linearly with nodes, so sizing at 70 percent utilisation rather than 90 is not conservatism but an acknowledgement of the queueing term.

Elasticsearch's shard-sizing guidance (target tens of gigabytes per shard rather than a fixed count) is the vendor version of the "does it fit on one node" question, and its existence reflects how often over-sharding is the actual problem.

The quantisation figures come from the standard HNSW memory formula (dimensions times bytes per component, plus roughly eight times M for the graph), which is documented in hnswlib and reproduced in every vector database's capacity guidance.

The debate

The case for doing the arithmetic first: it prevents designing for the wrong bottleneck, which is the most common and most expensive design error. A distributed system built for a workload that fits on one machine carries permanent complexity for no benefit.

The case against over-indexing on it: the numbers are assumptions, real workloads are skewed in ways averages hide, and a design justified by arithmetic that turns out to be wrong is still wrong. Measurement beats estimation whenever measurement is available.

My position: do the arithmetic, state the assumptions, and name which one is load-bearing.

The part I would insist on is the last one. Any sizing rests on assumptions and only one or two of them change the architecture, so identifying those and saying so is more valuable than the numbers themselves. In the worked example, tripling the user count changes nothing and tripling the catalogue size forces sharding, and knowing that tells you exactly what to verify before committing.

The specific discipline I would apply is checking whether it fits on one node before designing a distributed system. The 82 GB figure is the whole design: it means nine replicas rather than a sharded cluster, no scatter-gather, and no tail-at-scale problem. And notice the direction of causation, which is unusual: the int8 quantisation decision is what keeps it under the threshold, so an optimisation removed the need for a distributed architecture rather than the architecture coming first and the optimisation later.

Where I would push back on the exercise: an average is a poor summary of a skewed workload, and search traffic is extremely skewed. Four hundred QPS average hides that the top 1 percent of queries are typically 30 to 50 percent of traffic, which means a cache is worth far more than the average suggests, and that a p99 sized from average service time will be wrong. The arithmetic gives you the shape; the distribution gives you the tail, and I would say which one I am answering.

Follow-up Q&A

"How do you approach a sizing question?" State assumptions explicitly so they can be corrected, work in round numbers because the answer only needs to be right within a factor of two, and then derive the architecture from the result rather than the other way round. The sentence that matters is "X is Y, therefore Z". A candidate who computes numbers and then designs something unrelated to them has treated the arithmetic as decoration.

"What decided the architecture in that example?" The storage figure. Eighty-two gigabytes fits on one node, so this is nine replicas each holding the full index, not a sharded cluster, which means no scatter-gather and therefore no tail-at-scale problem. And what is interesting is the direction: at fp32 the vectors alone are 154 gigabytes and it does not fit, so the int8 quantisation decision is what removed the need for sharding. An optimisation determined the architecture rather than following from it.

"Which assumption matters most?" The catalogue size, and I would say so unprompted. If users triple, 3,600 QPS still fits about 25 nodes and nothing structural changes. If the catalogue triples to 150 million items, the index is 156 gigabytes and it no longer fits one node, so it becomes a sharded system with all the scatter-gather latency that implies. Knowing which number is load-bearing tells the interviewer exactly what to probe and tells me what to verify before committing.

"How do you get from QPS to fleet size?" Little's Law: concurrency equals arrival rate times service time. Twelve hundred QPS at 65 milliseconds is 78 concurrent requests. Then divide by the target utilisation, and I would never size above 70 percent, because queueing time is service time times rho over one minus rho, so at 80 percent utilisation you are waiting four times the service time and at 90 percent, nine times. That gives 112 slots, and at roughly 170 requests per second per 16-core node it is seven nodes, rounded to nine for three-AZ balance.

"Why round 86,400 to 100,000?" Because the answer needs to be right within a factor of two and the rounding introduces 16 percent error, which is far smaller than the uncertainty in the assumptions themselves. Meanwhile it makes the arithmetic doable in your head without mistakes, and an arithmetic slip in an interview costs more credibility than a 16 percent imprecision. The numbers to have are 100,000 seconds a day and 2.5 million a month.

"Where does the average mislead you?" Search traffic is extremely skewed, so 400 QPS average hides that the top one percent of queries are typically 30 to 50 percent of volume. That means a cache is worth much more than an average-based calculation suggests, and a p99 estimated from average service time will be wrong because the tail is driven by the slow minority. The arithmetic gives you the shape and the distribution gives you the tail, and I would be explicit about which one I am answering.

"When would you not do this?" When measurement is available. This is design-time sizing to establish which constraint binds, not capacity planning. If the system exists, its actual p99, its actual cache hit rate and its actual query distribution beat any estimate, and the right move is to measure rather than to argue from assumptions.

"What is the most common error you see?" Designing for the wrong bottleneck. A million jobs a day is twelve per second and ten gigabytes, which is one Postgres instance, and teams reach for a distributed database. Two million vectors at 256 fp16 features is one gigabyte, which fits in every serving process, and teams put it in a remote store and then discover the lookup rate is unservable. In both cases thirty seconds of arithmetic settles it and the design conversation changes completely.

Common misconceptions

"Precision matters." The answer needs to be right within a factor of two. Precision beyond that costs time and invites arithmetic errors.

"Size for peak." Size for peak at a target utilisation well below saturation, because queueing time explodes as utilisation approaches one.

"More nodes is safer." More nodes means more scatter-gather and a worse tail. Check whether it fits on one node first.

"The average tells you the shape." For skewed workloads it does not. The average sizes the fleet and the distribution sizes the tail.

"Do the arithmetic, then design." Do the arithmetic and let it determine the design. If the numbers and the architecture are unrelated, the arithmetic was decoration.

Interview delivery note

Announce the method before doing it, because the structure is part of what is being scored: "I'll state assumptions explicitly so you can correct them, work in round numbers because this only needs to be right within a factor of two, and then let the numbers pick the architecture."

Then make the derivation audible: "Twenty million monthly actives at five searches on ten active days is a billion a month, which over two and a half million seconds is 400 QPS average and about 1,200 at peak. That's not a large number, which already tells me this isn't a throughput problem."

Land the decisive number and say what it decides: "Fifty million items gives a thirty gigabyte lexical index, and int8 vectors plus the HNSW graph is fifty-two, so eighty-two gigabytes total. That fits on one node, so this is nine replicas each holding everything, not a sharded cluster, and there's no scatter-gather and no tail-at-scale problem. And notice that at fp32 the vectors alone are a hundred and fifty-four gigabytes and it doesn't fit, so the quantisation decision is what removed the need to shard."

Then the move that most candidates skip: "The assumption that's load-bearing is the catalogue size. Triple the users and nothing structural changes; triple the catalogue and it no longer fits one node and this becomes a sharded system. So that's the number I'd want to be right about."

And show calibration on the method itself: "though the average hides a lot here. Search traffic is heavily skewed, the top one percent of queries is usually thirty to fifty percent of volume, so a cache is worth much more than this calculation implies and the p99 won't come from the average service time."

Further reading

  • Jeff Dean's "Building Software Systems at Google and Lessons Learned" (Stanford, 2010), for the numbers and the practice.
  • Little, "A Proof for the Queuing Formula L = λW" (1961), and Gunther's Guerrilla Capacity Planning for the utilisation caution.
  • Elasticsearch's "Size your shards" guidance, for the does-it-fit-on-one-node discipline in a specific system.
  • The hnswlib documentation for the vector index memory formula used above.