Capacity Estimation and Back-of-the-Envelope Analysis

Published by Programming Line in System Design and Distributed Systems. Last updated .

Purpose: Turning Scale Into Design Inputs

Capacity estimation is the discipline of converting product goals such as “support a million users” into engineering numbers: requests per second, storage growth, cache size, network bandwidth, queue depth, shard count, and machine count. Back-of-the-envelope analysis is intentionally approximate. The outcome is not a perfect forecast; it is a defensible first model that exposes the dominant constraints before you choose databases, partitioning schemes, replication factors, or cache policies.

In system design work, these estimates sit between requirements and architecture. If a messaging service receives 50 writes per second, a single relational database may be reasonable. If it receives 500,000 writes per second with fanout to followers, the design conversation changes immediately. The estimate tells you which problems are real at the target scale and which would be premature complexity.

Mechanism: From Users to Bottlenecks

A capacity estimate usually moves through five layers. First, define the product action: upload a photo, post a message, fetch a home feed, start a video stream, or search an index. Second, estimate the event rate for that action. Third, split average load from peak load, because systems fail at peaks. Fourth, translate events into resources: CPU work, memory, storage, network traffic, database reads, database writes, and external calls. Fifth, add replication, retention, indexing, compression, and safety margins.

The core terminology is simple but precise. Daily active users, or DAU, counts users who perform at least one action per day. Requests per second, or RPS, is actions divided across time. Queries per second, or QPS, often refers to read queries against storage or search systems. Write throughput is the sustained rate of durable mutations. Fanout is the multiplier created when one event causes many downstream operations, such as sending a post to follower feeds. Retention is how long data must be kept. Replication factor is the number of copies stored for availability and durability. Headroom is unused capacity reserved for bursts, deploys, retries, and uneven traffic.

The most important internal step is unit conversion. A day has 86,400 seconds. One million events per day is about 11.6 events per second on average. A 10x peak factor turns that into 116 events per second. A 2 KB item written 100 times per second is 200 KB/s of logical data, but three replicas and two secondary indexes can make the physical write load several times larger. This multiplication is where many weak designs hide their cost.

Anatomy of a Capacity Model

A compact model should state assumptions, formulas, intermediate values, and design implications. Avoid a naked final number. A useful estimate says, for example: “100 million DAU, each reading 20 timelines per day, creates 2 billion timeline reads per day, or 23,148 average RPS. With a 6x peak factor, design the read path for about 140,000 RPS before retries.” That sentence gives reviewers a way to challenge the inputs without losing the reasoning.

SECONDS_PER_DAY = 86_400


def average_rps(events_per_day: float) -> float:
    return events_per_day / SECONDS_PER_DAY


def peak_rps(events_per_day: float, peak_factor: float) -> float:
    return average_rps(events_per_day) * peak_factor


def storage_gb(items_per_day: float, bytes_per_item: int, days: int, replicas: int = 1) -> float:
    return items_per_day * bytes_per_item * days * replicas / 1_000_000_000

print(round(average_rps(1_000_000), 2))
print(round(peak_rps(1_000_000, 10), 2))
print(round(storage_gb(2_000_000, 2048, 30, replicas=3), 1))

This code is not a replacement for measurement. It is a guardrail against arithmetic drift. Its deterministic output is 11.57 average RPS, 115.74 peak RPS, and 368.6 GB for the example storage calculation.

Worked Example 1: A URL Shortener

Start with a small read-heavy service. Assume 10 million shortened links are created per month and each link receives 20 redirects during its lifetime. Creation traffic is 10 million writes per month, which is roughly 3.9 writes per second on average. Redirect traffic is 200 million reads per month, about 77 reads per second on average. With a 10x peak factor, the service should tolerate roughly 40 write RPS and 770 redirect RPS.

Storage is modest. If each mapping stores a short code, long URL, owner id, timestamps, status, and metadata, assume 500 bytes per row before index overhead. Ten million rows per month is about 5 GB of logical data. With three replicas and index overhead, planning for 20 to 30 GB per month is more realistic. The design implication is that lookup latency and cache hit rate matter more than raw storage. A cache for hot short codes can absorb bursts, while a durable database remains the source of truth.

Worked Example 2: Image Upload and Delivery

Now consider a photo feature with 5 million DAU. Suppose 10 percent upload two photos per day, and every active user views 80 photos per day. Uploads are 1 million images per day, or 11.6 image writes per second on average. Viewing produces 400 million image reads per day, or 4,630 average read RPS. A 5x peak means about 23,000 image reads per second.

If the original image averages 3 MB and derived thumbnails add another 500 KB, daily new object storage is about 3.5 TB before replication or erasure coding. Thirty days of new images is about 105 TB. The dominant design choices become object storage, CDN caching, asynchronous thumbnail generation, and metadata indexing. Serving binary media through application servers would waste compute and network capacity. The application should issue references or signed URLs, while the CDN and object store carry the heavy bytes.

Worked Example 3: Social Feed Fanout

A social feed adds a hidden multiplier. Assume 20 million DAU, each posting 0.2 times per day on average. That is 4 million posts per day, or 46 average write RPS. At first glance, this seems small. If each author has 300 followers and the system materializes follower inboxes on write, fanout creates 1.2 billion feed insertions per day, or 13,889 average insertion operations per second. With a 6x peak, the fanout workers must handle about 83,000 insertions per second.

The output of the estimate changes the architecture. Fanout-on-write gives fast home feed reads but expensive writes and queue pressure. Fanout-on-read reduces write amplification but makes reads slower for users who follow many active accounts. A hybrid design often treats ordinary accounts with fanout-on-write and celebrity accounts with fanout-on-read or ranked retrieval. The estimate identifies where that threshold might sit.

posts_per_day = 4_000_000
average_followers = 300
peak_factor = 6

feed_insertions_per_day = posts_per_day * average_followers
average_insertions_per_second = feed_insertions_per_day / 86_400
peak_insertions_per_second = average_insertions_per_second * peak_factor

print(round(average_insertions_per_second))
print(round(peak_insertions_per_second))

The expected output is 13889 and 83333. These numbers explain why the queue and worker pool are central components, even though the original post rate is only dozens of writes per second.

Design Choices and Trade-offs

Peak factor is a judgment call. Consumer apps often have daily cycles and event-driven spikes, while enterprise tools may have narrower office-hour peaks. Too low a peak factor causes overload during predictable bursts. Too high a factor can push the design toward unnecessary sharding or expensive managed services. A good estimate records the factor and the reason for it.

Safety margin is different from peak factor. Peak factor models traffic shape; safety margin covers uncertainty, retries, uneven key distribution, deploy overlap, and degraded nodes. For early design, 30 to 100 percent headroom is common depending on confidence and blast radius. When a component has a hard limit, such as maximum connections or partition throughput, use the limit directly instead of assuming smooth scaling.

Logical and physical size also differ. Logical size is the user-visible payload. Physical size includes serialization overhead, indexes, replicas, tombstones, compression effects, write-ahead logs, compaction slack, and backups. A database with two secondary indexes may spend more bytes maintaining access paths than storing the original record. Capacity analysis should name both when storage cost or write throughput matters.

Failure Modes and Troubleshooting

A common symptom is that average RPS looks safe, but latency spikes at noon or during notifications. The cause is usually an underestimated peak factor or synchronized clients. Diagnose it by graphing per-minute request rates, retry rates, and queue depth rather than only daily totals. Correct it with rate limiting, jittered client scheduling, autoscaling based on leading indicators, and enough headroom for the measured peak.

Another failure is storage growing faster than predicted. Symptoms include partitions filling, backup windows expanding, compaction lag, or object storage bills exceeding plan. Causes include forgotten replicas, larger real payloads, retained derived objects, verbose metadata, or indexes omitted from the estimate. Diagnose by sampling actual row sizes, index sizes, object sizes, and retention classes. Correct the model, then apply lifecycle policies, compression, index removal, or partition expansion.

A third failure is fanout backlog. Users may see stale feeds even though the post API succeeds. The cause is that write admission accepted more fanout work than workers could drain. Diagnose queue age, not just queue length, because a long queue of cheap tasks differs from a short queue of stuck tasks. Correct it by separating heavy authors, increasing worker concurrency within downstream limits, batching writes, or changing the fanout strategy.

Security, Performance, and Reliability Implications

Capacity estimates affect security because abuse often appears as unexpected load shape. A shortener must budget for malicious redirect bursts and link creation spam. A media service must handle hotlinking and unauthorized scraping. Estimating per-user and per-token rates helps set throttles that protect shared databases and object stores without blocking normal users.

Performance depends on locating the dominant resource. For feeds it may be write amplification; for image delivery it is bandwidth and cache fill; for search it may be index memory and query CPU. Reliability depends on capacity under degraded conditions. If one availability zone is lost, remaining zones must absorb the shifted traffic. If retries double request load during a dependency outage, the estimate should include retry budgets and circuit breakers.

Hands-on Lab: Estimate a Notification Service

Prerequisites: a terminal with Python 3, and a text editor or REPL. No external services are required.

  1. Define the requirement: 8 million DAU, each triggering 3 notifications per day, with each notification delivered to one device on average.
  2. Calculate average notification creation RPS and choose a peak factor of 8 for morning and evening bursts.
  3. Estimate queue throughput if 15 percent of users have three devices instead of one.
  4. Estimate storage for 14 days of notification history, assuming 700 bytes per notification and three replicas.
  5. Compare the result with a worker pool where each worker can send 250 notifications per second.
dau = 8_000_000
notifications_per_user_per_day = 3
peak_factor = 8
multi_device_fraction = 0.15
extra_devices_for_multi_device_users = 2
bytes_per_notification = 700
retention_days = 14
replicas = 3
worker_capacity_per_second = 250

events_per_day = dau * notifications_per_user_per_day
device_multiplier = 1 + multi_device_fraction * extra_devices_for_multi_device_users
deliveries_per_day = events_per_day * device_multiplier
average_delivery_rps = deliveries_per_day / 86_400
peak_delivery_rps = average_delivery_rps * peak_factor
history_gb = events_per_day * bytes_per_notification * retention_days * replicas / 1_000_000_000
workers_needed = peak_delivery_rps / worker_capacity_per_second

print(round(average_delivery_rps))
print(round(peak_delivery_rps))
print(round(history_gb, 1))
print(round(workers_needed))

Verification: the output should be 361 average delivery RPS, 2889 peak delivery RPS, 705.6 GB of replicated history, and 12 workers before extra headroom. A practical design would provision more than 12 workers because downstream push providers, retries, rolling deploys, and uneven traffic reduce effective capacity. Cleanup is simply deleting the scratch script or closing the REPL; no infrastructure was created.

Assessment Exercises

  1. A chat app has 2 million DAU, each sending 40 messages per day. Group messages average 5 recipients. Estimate average delivery RPS and explain which number is more relevant: message writes or recipient deliveries.
  2. A search service indexes 50 million new documents per day at 4 KB each. It stores three replicas and keeps 30 days online. Estimate storage, then name two reasons the real footprint may differ.
  3. A feed system has low post RPS but high queue age. Use capacity terminology to explain how this can happen and what metric you would inspect first.
  4. You estimated peak traffic with a 3x factor, but production shows 12x during marketing campaigns. Describe two design changes and one process change that would prevent repeated overload.
  5. For a read-heavy product page service, decide whether cache memory, database write throughput, or object storage is likely to dominate. State assumptions that could change your answer.

Summary

Capacity estimation turns system design from vague scale claims into concrete engineering constraints. The method is to state assumptions, convert units, separate average from peak, multiply by fanout and replication, and translate the result into component limits. Strong back-of-the-envelope work does not pretend to be exact. It makes the next design decision sharper: whether to cache, shard, queue, batch, denormalize, use object storage, or change the product workflow before the architecture is overloaded.