Mert Özen Further With Every Line
Software Architecture System Design — #02

Back-of-the-Envelope Estimation: QPS, Storage, Bandwidth and Availability Math

Mert Özen Nov 8, 2026 13 min 4 views
Back-of-the-Envelope Estimation: QPS, Storage, Bandwidth and Availability Math

Back-of-the-envelope estimation is a short chain of calculations from DAU to average and peak QPS, and from there to storage, bandwidth and availability, that gets the order of magnitude right. For a URL shortener with 10 million DAU, ~1,700 peak QPS and ~1 TB over 5 years fit one database, while 55.6 Gbps of egress makes a CDN mandatory for a photo app; 99.9% means 8.76 hours of downtime a year, and availability multiplies across serial components.

Version note: The code in this post was built and run with C# 14 and .NET 10.

Guess First

Picture two products, each with 10 million daily active users. One is a URL (Uniform Resource Locator) shortener: users shorten long links, other people click the short link and get redirected. The other is a photo sharing app: users upload photos and see other people's photos in their feed.

Three questions. For the URL shortener's redirect traffic, how many requests per second are we talking about: a hundred, a thousand, a hundred thousand? Will five years of the link table fit on one server's disk? In the photo app, what struggles first: the database, the disk or the network?

Answer by gut feeling, then compare with the calculation below. The whole point of this post is to replace that gut feeling with a five-minute calculation.

Why Calculate on Paper?

Back-of-the-envelope estimation means estimating a system's load and resource needs with rough numbers that are right in order of magnitude. The name comes from the "calculation on the back of an envelope" idea Jeff Dean popularised at Google: the goal is not to find 1,736 but to know whether the answer is around 2,000 or around 200,000. The gap between those two numbers is the gap between a single database and a sharded cluster.

In the previous post, "What Is System Design and How Do You Approach It? A 4-Step Method from Requirements to Deep Dive", we found the campaign day bottleneck on paper with Little's law. Here we make the same discipline systematic: from the number of DAU (Daily Active Users) to requests per second, and from there to storage, bandwidth and availability.

The Alphabet of Numbers: Powers of Two and Latency Numbers

Two tables should be in your head before you start. The first is sizes. Computers count in binary, but for estimation rounding in base ten is accurate enough: 2 to the 10th = 1,024 ≈ a thousand.

2^10 ~ 1 thousand     1 KB    a short JSON response
2^20 ~ 1 million      1 MB    a photo, compressed
2^30 ~ 1 billion      1 GB    fits in RAM easily
2^40 ~ 1 trillion     1 TB    one disk
2^50 ~ 1 quadrillion  1 PB    a storage cluster

int 4 B, long / DateTime 8 B, Guid 16 B, ASCII char 1 B, UTF-16 char 2 B

The percentage gap grows with size (2.4% at KB, about 10% at TB), so precise capacity planning uses KiB/GiB; for estimation base ten is enough. The code in this post also takes 1 KB = 1,000 bytes.

The second is latency. The rows that matter for system design from the "latency numbers every programmer should know" list compiled by Jeff Dean and popularised by Peter Norvig (the numbers are from the early 2010s; today's hardware is faster but the ratios still guide you):

OperationTimeRoughly
Main memory reference100 nsmemory
Send 1 KB over a 1 Gbps network10 µs100 × memory
Read 4 KB randomly from an SSD (Solid State Drive)150 µs1,500 × memory
Read 1 MB sequentially from memory250 µs
Round trip within the same datacenter500 µshalf a millisecond
Read 1 MB sequentially from an SSD1 ms4 times slower than memory
Disk seek10 ms
Packet California → Netherlands → California150 msintercontinental

Three lessons from the table are enough. Memory is orders of magnitude faster than the network and disk; that is why caches exist. A round trip within the same datacenter is half a millisecond; if a page makes 20 service calls one after another, the network alone takes 10 ms. An intercontinental round trip is 150 ms; you cannot optimise the speed of light, so being close to the user needs a CDN (Content Delivery Network) and regional deployments. An SSD is about 70 times faster than a disk seek for random reads.

From DAU to QPS: Average and Peak

The traffic calculation is always the same chain: number of users × operations per user per day = operations per day; operations per day ÷ 86,400 seconds = average QPS (queries per second). In the industry QPS and RPS (requests per second) are mostly used interchangeably; here both mean "requests per second".

For quick math take 1 day ≈ 10^5 seconds (86,400 in reality); 1 million operations a day ≈ 10–12 operations per second. Knowing this lets you say "~1,200 per second" in your head when you hear "100 million requests a day".

The average alone is useless, because traffic is not spread evenly over the day. The honest way to estimate the peak is to assume a traffic shape: if half of the daily traffic arrives in 4 evening hours, the rate in the peak hours is 0.5 / (4/24) = 3 times the average. In this post we took the peak factor as 3 this way. For events like a campaign, a match result or a push notification this multiplier easily goes past 10; Bookstore's 10 times on campaign day is an example. You plan capacity for the peak, not the average.

Calculating reads and writes separately is also a must. The read:write ratio is the first number that tells you how much a cache will help and whether the database needs replicas or sharding.

Storage and Bandwidth

Storage: new records per day × record size × retention. When estimating record size count not just the fields but also indexes and row overhead; 2–3 times the raw data is a practical estimate. If there is replication (a replica in the database, usually 3 copies or erasure coding in object storage), multiply the raw need accordingly.

Bandwidth: QPS × request or response size. Ingress (into the system) and egress (out of the system) are calculated separately; at cloud providers egress is billed and is often the source of an unexpected invoice. Don't forget to multiply by 8 to turn bytes into bits: network capacity is talked about in bits per second.

End to End: URL Shortener and Photo Sharing

Let's write the assumptions into code and let it calculate. What matters is not the code but that every line's assumption is visible:

const double SecondsPerDay = 86_400;
const double PeakFactor = 3; // 50% of daily traffic in a 4-hour evening window: 0.5 / (4 / 24.0)

Console.WriteLine("== URL shortener, 10M DAU");
double redirectsPerDay = 10_000_000 * 5.0;     // each user follows 5 short links a day
double newLinksPerDay = 10_000_000 * 0.10;     // 10% of users create one link a day
Rate redirects = new(redirectsPerDay / SecondsPerDay, PeakFactor);
Rate writes = new(newLinksPerDay / SecondsPerDay, PeakFactor);
Console.WriteLine($"read QPS         avg {redirects.Average,8:N0}   peak {redirects.Peak,8:N0}");
Console.WriteLine($"write QPS        avg {writes.Average,8:N1}   peak {writes.Peak,8:N1}");
Console.WriteLine($"read:write       {redirectsPerDay / newLinksPerDay:N0}:1");
double rows = newLinksPerDay * 365 * 5;
Console.WriteLine($"rows in 5 years  {rows:N0}");
Console.WriteLine($"storage 5 years  {Size.Format(rows * 500)}   (500 B per row incl. index)");
Console.WriteLine($"base62 codes     6 chars = {Math.Pow(62, 6):N0}, 7 chars = {Math.Pow(62, 7):N0}");
Console.WriteLine($"egress at peak   {Size.Format(redirects.Peak * 300)}/s = {redirects.Peak * 300 * 8 / 1e6:N1} Mbps (300 B per redirect)");
Console.WriteLine($"cache (20% hot)  {Size.Format(redirectsPerDay * 0.20 * 500)}");

Console.WriteLine();
Console.WriteLine("== Photo sharing, 10M DAU");
double uploadsPerDay = 10_000_000 * 0.10 * 2;  // 10% of users upload 2 photos a day
double viewsPerDay = 10_000_000 * 100.0;       // each user sees 100 thumbnails a day
Rate uploads = new(uploadsPerDay / SecondsPerDay, PeakFactor);
Rate views = new(viewsPerDay / SecondsPerDay, PeakFactor);
Console.WriteLine($"upload QPS       avg {uploads.Average,8:N1}   peak {uploads.Peak,8:N1}");
Console.WriteLine($"view QPS         avg {views.Average,8:N0}   peak {views.Peak,8:N0}");
double perPhoto = 2_000_000 + 500_000;         // original 2 MB + resized variants 0.5 MB
Console.WriteLine($"blob storage     {Size.Format(uploadsPerDay * perPhoto)}/day, {Size.Format(uploadsPerDay * perPhoto * 365)}/year");
Console.WriteLine($"metadata         {Size.Format(uploadsPerDay * 1_000 * 365)}/year (1 KB per photo)");
Console.WriteLine($"ingress at peak  {uploads.Peak * 2_000_000 * 8 / 1e9:N2} Gbps");
Console.WriteLine($"egress avg/peak  {views.Average * 200_000 * 8 / 1e9:N1} / {views.Peak * 200_000 * 8 / 1e9:N1} Gbps (200 KB thumbnail)");

public readonly record struct Rate(double Average, double PeakFactor)
{
    public double Peak => Average * PeakFactor;
}

Size.Format is a small helper that turns bytes into KB, MB, GB and TB in steps of 1,000. Output:

== URL shortener, 10M DAU
read QPS         avg      579   peak    1,736
write QPS        avg     11.6   peak     34.7
read:write       50:1
rows in 5 years  1,825,000,000
storage 5 years  912.5 GB   (500 B per row incl. index)
base62 codes     6 chars = 56,800,235,584, 7 chars = 3,521,614,606,208
egress at peak   520.83 KB/s = 4.2 Mbps (300 B per redirect)
cache (20% hot)  5 GB

== Photo sharing, 10M DAU
upload QPS       avg     23.1   peak     69.4
view QPS         avg   11,574   peak   34,722
blob storage     5 TB/day, 1.83 PB/year
metadata         730 GB/year (1 KB per photo)
ingress at peak  1.11 Gbps
egress avg/peak  18.5 / 55.6 Gbps (200 KB thumbnail)

The answers to the opening questions are here. The URL shortener's redirect traffic is ~1,700 per second at peak; a single, well-indexed relational database can handle that, but the 50:1 read:write ratio says this is exactly where a cache works best: the hottest 20% of the day's links fit in a single Redis instance at 5 GB. Five years means 1.8 billion rows and about 1 TB of data, which fits on one server's disk; you think about sharding later if write rate or availability calls for it, not storage. A 6-character base62 code covers 56 billion links, so you do not even need 7 characters. Bandwidth is negligible: 4.2 Mbps.

In the photo app the picture flips. The number of uploads is small (~70 per second at peak), but 5 TB of blobs pile up per day and 1.8 PB (petabyte) per year; that is a job for object storage, not a database, and only 730 GB a year of metadata stays in the database. The real bottleneck is the network: thumbnail egress is 18.5 Gbps on average and 55.6 Gbps at peak. Serving that from your own servers is not realistic; delivering images through a CDN is not an optimisation but a precondition of the design. This is also the answer to the third opening question: the network struggles first.

Availability: Nines, Serial and Parallel Components

Availability is the fraction of time a system can do the work it is asked to do. The "Embracing Risk" chapter of Google's SRE (Site Reliability Engineering) book describes two ways to measure it: time-based (uptime / total time) and, more meaningful for distributed systems, request-based (successful requests / total requests). The target is written as an SLO (Service Level Objective), while a promise made to a customer whose breach has consequences is an SLA (Service Level Agreement). "Nines" is the short name for this ratio:

public static string Downtime(double availability, TimeSpan period)
{
    TimeSpan down = period * (1 - availability);
    return down.TotalHours >= 1 ? $"{down.TotalHours:0.##} h" : down.TotalMinutes >= 1 ? $"{down.TotalMinutes:0.##} min" : $"{down.TotalSeconds:0.#} s";
}

// serial: every part must be up
public static double Serial(params double[] parts) => parts.Aggregate(1.0, (acc, a) => acc * a);

// parallel: at least one replica must be up
public static double Parallel(params double[] replicas) => 1 - replicas.Aggregate(1.0, (acc, a) => acc * (1 - a));
     99%  down 87.6 h         a year, 7.2 h        a month
   99.9%  down 8.76 h         a year, 43.2 min     a month
  99.95%  down 4.38 h         a year, 21.6 min     a month
  99.99%  down 52.56 min      a year, 4.32 min     a month
 99.999%  down 5.26 min       a year, 25.9 s       a month
serial   LB 99.99% -> app 99.95% -> DB 99.95% = 99.89%
parallel 2 x app 99.5%                      = 99.9975%

99.9% means 8.76 hours of downtime a year, 43 minutes a month; each extra nine cuts the allowed downtime ten times and raises the cost roughly by the same factor. Bookstore's checkout target is 99.9% a month: 43 minutes a month. A weekly deployment that causes five minutes of downtime each time eats half of that budget on its own.

The multiplication rule matters more. If a request passes through a load balancer (LB), the application and the database in sequence, all of them must be up and their availabilities multiply: a chain of three components at 99.99%, 99.95% and 99.95% individually drops to 99.89%, so the chain is worse than its weakest link. Parallel components work the other way: at least one of two instances that are 99.5% each being up gives 99.9975%. The condition is that the instances fail independently; if both sit in the same rack, depend on the same database or arrive with the same broken deployment, the product stays theoretical. Redundancy is worth only as much as the independence of the failures.

Average Latency Lies: p50, p95, p99

Latency targets are written with percentiles, not averages. A simulation of 10,000 search requests where 97% return in 30–70 ms and 3% take a slow path at 0.9–1.5 seconds:

Random rng = new(42);
double[] latencies = [.. Enumerable.Range(0, 10_000)
    .Select(_ => rng.NextDouble() < 0.97 ? 30 + rng.NextDouble() * 40 : 900 + rng.NextDouble() * 600)
    .Order()];
Console.WriteLine($"average {latencies.Average(),6:N0} ms");
Console.WriteLine($"p50     {Percentile(latencies, 0.50),6:N0} ms");
Console.WriteLine($"p95     {Percentile(latencies, 0.95),6:N0} ms");
Console.WriteLine($"p99     {Percentile(latencies, 0.99),6:N0} ms");
foreach (int calls in (int[])[1, 10, 100])
    Console.WriteLine($"page with {calls,3} backend calls: {(1 - Math.Pow(0.99, calls)) * 100,5:0.0}% of pages wait for a p99 call");

static double Percentile(double[] sorted, double p) => sorted[(int)Math.Ceiling(p * sorted.Length) - 1];
average     81 ms
p50         51 ms
p95         69 ms
p99      1,262 ms
page with   1 backend calls:   1.0% of pages wait for a p99 call
page with 10 backend calls:   9.6% of pages wait for a p99 call
page with 100 backend calls:  63.4% of pages wait for a p99 call

The average is 81 ms and describes no request at all: fast requests take 30–70 ms, slow ones more than a second, and not a single request returns in 81 ms. Even p95 looks fine at 69 ms; only p99 shows the slow path: 1.26 seconds. In a system that takes 50 million requests a day, 1% is 500,000 slow requests a day.

The last three lines are the math behind the argument in Jeff Dean and Luiz André Barroso's paper "The Tail at Scale" (Communications of the ACM (Association for Computing Machinery), 2013). If a page makes 100 backend calls in parallel and each call is slow with a 1% chance, 63% of pages wait for at least one slow call. As fan-out grows, the latency the user sees is set by the backends' p99 or even p99.9. Not setting a p99 target on a page built from microservices leaves the user experience to luck.

Rounding Discipline

The point of estimation is speed, and I keep a few rules for that. Round numbers to 1–2 significant digits: 10^5 instead of 86,400, "~2,000" instead of 1,736. Always write an assumption with its unit and keep it next to the number: "5 redirects per user per day". Move the calculation in order, from average to peak, from raw data to the replicated and indexed size; if you skip a step you cannot find where you made the mistake. Finally, end with a comparison: "fits on one server", "one Redis is enough", "a CDN is a must". What matters is not the number but the decision it drives.

A warning: rounding errors compound. If each of five assumptions is off by a factor of 2, the result can be off by a factor of 32. That is why choosing reasonable, not precise, assumptions and saying which ones affect the result most is worth more than giving a single "exact" number. In the photo example the assumption that decides the result is the thumbnail size: a 50 KB WebP thumbnail instead of 200 KB cuts egress to a quarter.

Common Pitfalls

Forgetting the peak. A system sized for the average QPS falls short in the hour the traffic arrives; write the peak factor explicitly in the assumption list.

Mixing up average latency and p99. "Our average response time is 80 ms" can hide hundreds of thousands of requests taking longer than a second.

Mixing up bytes and bits. There is a factor of 8 between 55.6 Gbps and 55.6 GB/s; network capacity is discussed in bits, storage in bytes.

Not counting indexes and replication. If the raw data is 300 GB, with indexes, row overhead, a replica and backups the real need easily goes past 1 TB.

Adding up availability for serial components, or taking the lowest one. The chain multiplies and ends up worse than the weakest link.

Doing the math as decoration. If the result does not lead to a decision ("fits in Redis", "object storage and a CDN are a must"), the estimation was never done.

A Small Experiment

★☆☆ Set the peak factor to 10 instead of 3 (campaign day) and recalculate write and read QPS for the URL shortener. Do you still think a single database is enough?

★★☆ In the photo app make the thumbnail 50 KB instead of 200 KB and raise daily views per user to 300. How many Gbps is peak egress now? Which assumption affected the result more?

★★★ Calculate availability for Bookstore's checkout chain: load balancer 99.99%, two application instances at 99.5% each, SQL (Structured Query Language) Server 99.95%, payment provider 99.9%. How does the order becoming "PaymentPending" when the payment provider does not respond (a decision Bookstore made) change checkout's availability? Use the serial and parallel formulas together.

We now have the numbers: we can say with figures where a single server will not be enough. In the next post we look at what happens after that point: vertical and horizontal scaling, making the application tier stateless, and the six-step architecture evolution from one box to multiple regions.