All posts

Capacity Planning Is Multiplication

#architecture#system-design#scaling#infrastructure

You can get a rough first-draft size for almost any system from one input, monthly active users, using a short chain of multiplications and a handful of constants that have held up across published benchmarks for two decades. The point is not a precise answer; it is a defensible estimate that turns “big data” and “needs the cloud” into figures a reviewer can check, and that flags the architecture decisions you cannot skip. Treat the output as the right order of magnitude and the start of a conversation, not a forecast. Real numbers come from load-testing your own workload.

What problem does this solve?

“Big data,” “write-heavy,” and “needs the cloud” are not measurements, and a design built on them is a guess. The fix is a calculation chain that starts at users and ends at concrete component choices, so the vague terms get replaced with figures you can put in a design doc and have a reviewer check. The catch worth stating up front: the chain is only as good as its conversion ratios, and most of those ratios are empirical bands that vary by an order of magnitude across product categories. Use them as a defensible starting point you correct against real telemetry, not as physical constants.

The chain has one entry point and several outputs:

MAU → DAU → actions/day → avg QPS → peak QPS
                                       ├─→ storage/year
                                       ├─→ bandwidth → CDN?
                                       ├─→ working set → cache topology
                                       └─→ server count

Each output points at one decision, and to a first approximation the decisions are loosely coupled enough to reason about one at a time. That is a simplification, not a guarantee. In real systems the axes do interact (a cache changes your DB load, sharding changes your latency budget), so treat the chain as a way to get oriented, then expect second-order effects once you build.

How do you turn users into QPS?

Take monthly active users, multiply by a stickiness ratio to get daily active users, multiply by actions per user per day, divide by 86,400, then apply a peak factor. Peak QPS is the number every capacity tier is keyed to, so compute it first.

const SECONDS_PER_DAY = 86_400;

function peakQps(
  mau: number,
  stickiness: number,          // DAU/MAU, e.g. 0.4 for a social app
  actionsPerUserPerDay: number,
  peakFactor = 3.5,            // peak-to-average over a day
) {
  const dau = mau * stickiness;
  const avg = (dau * actionsPerUserPerDay) / SECONDS_PER_DAY;
  return Math.round(avg * peakFactor);
}

The stickiness ratio is DAU/MAU, the metric Facebook’s growth team popularized, and it is the input people most often get backwards. Consumer social and messaging apps are stickier than work tools, not less: Facebook has historically reported above 60%, Slack cites 40–50% for active workspaces, while the median B2B SaaS product sits near 13% in Mixpanel’s benchmark data because most B2B usage is workday-centric or episodic. Andrew Chen and Facebook’s early growth work treated 20% as the line where a product crosses into habitual use and 50% as the line where it becomes a utility.

The bands below are directional, drawn from public benchmark reports and vendor disclosures. They shift with how you define an “active” user, the product’s natural cadence, and the year, so anchor against your own historical DAU/MAU before trusting any row:

App category DAU/MAU (stickiness) Source signal
Messaging (WhatsApp, Slack, Discord) 50–65% Slack 40–50%, Facebook 60%+
Social / content feeds 30–50%+ Mature consumer apps
Consumer productivity (Notion, Todoist) 30–40% Daily-use tools
B2B SaaS, daily-use 20–30% Above the 20% habit line
B2B SaaS, weekly/episodic 10–15% Mixpanel median ≈ 13%
Ecommerce 5–15% Purchase cadence is low

For the peak factor: a single-region app with users in roughly one set of timezones runs a peak-to-average ratio around 3–4× over a day, per the Prometheus team’s capacity-planning guidance; spreading users globally flattens that, and event-driven spikes (a product launch, a match, a news drop) push it far higher. Steady e-commerce can sit as low as 1.2–2.0×. Pick the band that matches your traffic shape rather than reaching for one number.

Worked example with made-up but plausible inputs, a 10M-MAU social app: at an assumed 40% stickiness that is 4M DAU, and if you guess fifty reads and two writes per user that is 52 actions, so average QPS is (4_000_000 * 52) / 86_400 ≈ 2,400, and peak at 3.5× lands near 8,400 QPS. Every input there is an estimate, so read the output as “single-digit thousands of QPS at peak,” not 8,400 on the nose. Even so, the read:write split of 25:1 is robust to the guesswork and settles the storage engine: a B-tree store with replicas and a cache in front, not an LSM engine. That is the pattern to look for, a conclusion that survives the inputs being wrong by a factor of two.

How big does the data get in a year?

Multiply write QPS by 86,400 by the average record size, then by 365. The yearly figure is what tells you whether a single node survives or whether you are sharding from day one.

Take a chat app at 1M MAU, 60% stickiness so 600k DAU, roughly 400 actions a day. Average QPS is about 2,800, write QPS about half at 1,400. Then:

  • Daily writes: 1,400 × 86,400 ≈ 120M messages/day
  • At ~300 bytes each: 120M × 300 B ≈ 36 GB/day
  • Over a year: ~13 TB

Thirteen terabytes a year is a strong signal that one machine will struggle, so it is worth pressure-testing the sharding question in week one rather than discovering it at month nine. Worth a caveat: this assumes you keep every message forever in the hot store. Tiering cold data to object storage, compressing, or setting retention can change the picture by an order of magnitude, so the raw projection is an upper bound on what your primary store has to hold. The read:write ratio here is close to 1:1, write-heavy by chat standards, which leans toward an LSM-tree store like Cassandra or ScyllaDB over vanilla Postgres.

Record-size constants worth keeping on hand (order-of-magnitude, not exact):

Record Size
Chat message / tweet ~300 B
User profile row ~1 KB
Log line 500 B – 2 KB
Compressed image 100 KB – 2 MB
1080p video, per minute ~10 MB

When do you actually need a CDN?

Multiply read QPS by average payload size; if the result clears a single network interface, you need a CDN. This is the calculation that turns “we should probably use a CDN” into a number a reviewer can check.

An image-serving endpoint at 5,000 read QPS and 500 KB average payload pushes 5,000 × 500 KB = 2.5 GB/s, which is 20 Gbps. A standard server NIC runs 1–10 Gbps, with 25 to 100 Gbps available in datacenter configs, so origin-serving sustained traffic at that level from one box is impractical and a CDN starts to look mandatory. The averages hide the real risk, which is the peak: a viral spike or a hot object can blow past the average by far more than your peak factor suggests, and that burst is usually what takes the origin down. The multiplication gives you the steady-state case; size the CDN and origin for the spike.

How much cache, and where?

Take the hot fraction of the dataset and check whether it fits in one machine’s RAM. If it does, one cache node; if it does not, you are running a distributed cache and should plan for it now rather than discover it under load.

The hot fraction is not arbitrary. Web request popularity follows a Zipf-like distribution, shown across six proxy traces by Breslau et al. in 1999 and reconfirmed many times since, with the rank exponent typically between 0.6 and 0.85. In plain terms a small set of objects absorbs most of the requests, which is why caching works at all. The convenient “80/20” shorthand is an approximation of that curve, not a law; your real hot fraction depends on the exponent, and skewed access can be far more concentrated than 20%.

A 100 GB dataset with a 20% hot set is 20 GB, which fits in a single 32 GB box, so Redis on one node is the answer. Push the dataset to 2.5 TB and the hot set is 500 GB, past any single node, and you are on Redis Cluster whether you wanted the operational weight or not. If your access is closer to uniform than Zipf, the cache stops earning its place and the calculation tells you to spend the money elsewhere.

What can a single database node actually take?

Assume the low tens of thousands of simple transactions per second from one well-provisioned, well-tuned node, then benchmark your own workload before trusting any number, because throughput is workload-bound, not a fixed spec. AWS’s own pgbench run on Aurora PostgreSQL reached roughly 41,500 reads/sec and 166,000 writes/sec on large hardware, which is the ceiling end; a modest tuned instance lands well below that, and an untuned one with the wrong indexes lands lower still.

Component Rough single-node capacity
PostgreSQL, simple OLTP low tens of thousands TPS; tens of thousands of reads/sec tuned
Redis 100k+ ops/sec
App server (Go/Node, simple logic) 5–20k RPS
Nginx, static content 50k+ RPS

The honest rule: these are starting estimates for a sizing doc, and the only number you should stake a launch on is one you measured on your workload, your hardware, your query plan.

Why does getting the classification right make the rest mechanical?

Because each computed number maps cleanly onto one axis of the design, and to a first approximation you can reason about them separately. Read/write ratio leans on the storage engine. Data size shapes the architecture. The latency hierarchy informs where caches sit: a main-memory reference is about 100 ns, an SSD random read about 150 µs, so RAM is roughly 1,500× faster than SSD, and an HDD seek at about 10 ms is another ~65× beyond that (the canonical Jeff Dean / Peter Norvig numbers, kept current in Simon Eskildsen’s napkin-math repo). QPS suggests whether you distribute at all. None of this is mechanical in the sense of removing judgment; it narrows the search space and tells you which judgment calls actually matter, which is most of the value.

The failure mode is the one to keep in view: a stickiness estimate that is 3× off propagates straight to a server count that is 3× off, and the arithmetic looks just as confident while it does. The chain does not know your inputs are wrong. So treat every ratio here as an assumption with a citation rather than a fact, sanity-check the output against a system you already understand, and re-run it once you have real telemetry. Used that way it beats a guess. Used as a precise predictor it will burn you.

Key takeaways

  • These are thumb rules, not a model. The chain gives you an order of magnitude and a list of decisions to make, not a number to plan against. Validate with load testing.
  • One input, a chain of multiplications. MAU → DAU → QPS → peak, then storage, bandwidth, cache, and server count. Each output points at a single decision.
  • 86,400 is the constant you use most. Seconds per day is the divisor in nearly every capacity calculation.
  • Stickiness runs the opposite way people assume. Consumer social sits at 30–60%+ DAU/MAU; B2B SaaS often 10–25%, with a Mixpanel median near 13%. Bands vary by how you define “active.”
  • Peak-to-average is ~3–4× for a steady single-region app, flatter when globally distributed, much higher for event spikes. Size for the spike, not the average.
  • The yearly storage figure is an upper bound, before retention and tiering. ~13 TB/year on a chat app is a reason to pressure-test sharding early.
  • A CDN is a multiplication, checked against the spike. Read QPS × payload over a single NIC’s 1–10 Gbps ceiling means you likely need one.
  • Cache sizing rests on Zipf, not on 80/20. Request popularity is power-law (Breslau et al., 1999); 80/20 is a usable approximation of that curve, and skew varies.
  • Single-node DB throughput is workload-bound. Assume low tens of thousands of simple TPS, then benchmark; AWS’s Aurora pgbench hit ~41.5k reads/s on large hardware.

Run the chain on whatever you are building right now: take your real MAU, pick a stickiness ratio honestly from the table, and compute peak QPS before you touch a schema. Read the result as an order of magnitude, let it tell you which decisions are forced and which are still open, then go measure. The arithmetic gets you to a sensible starting point fast; it does not get you out of load-testing.

I’ve run this kind of estimate on real systems — including an enterprise logistics platform sized for 5,000+ daily shipment transactions. If you want a second pair of eyes on a capacity plan before you commit to an architecture, that’s part of what I offer as technical consultingget in touch.

Sources

  • Jeff Dean / Peter Norvig, “Latency Numbers Every Programmer Should Know” — canonical figures, maintained community copy at gist.github.com/jboner/2841832.
  • Simon Eskildsen, github.com/sirupsen/napkin-math — modern, benchmarked, actively-maintained successor to the latency-numbers list.
  • L. Breslau, P. Cao, L. Fan, G. Phillips, S. Shenker, “Web Caching and Zipf-like Distributions: Evidence and Implications,” INFOCOM 1999 — empirical basis for power-law request popularity and the hot-set argument.
  • Mixpanel Product Benchmarks — DAU/MAU stickiness medians by category (B2B SaaS ≈ 13%).
  • Robust Perception (Prometheus team), “Do you know your peak-to-mean ratio?” — peak-to-average of 3–4× over a day for typical single-region traffic.
  • AWS, “Amazon Aurora PostgreSQL Performance Assessment / Benchmarking” — pgbench and sysbench throughput figures on large hardware.

Get in touch

Have a project in mind or a role to fill? Tell me what you need and I'll get back to you.

I'll reply within 24 hours.

Prefer to talk live? Book a 15-min intro call