Free lesson · System Design Interviews
Step 2: Capacity Estimation
Now that we have requirements locked down, let's do the math. This step takes 2-3 minutes in an interview, and its purpose isn't precision - it's making sure your architecture is in the right ballpark. The difference between 1,000 QPS and 100,000 QPS determines whether you need one server or fifty. The difference between 10 GB and 10 TB determines whether you need sharding. Get the order of magnitude right, and your design will be sound.
I'll walk you through each calculation the way I'd do it live in an interview - thinking out loud, rounding aggressively, and connecting each number to the design decision it drives.
Traffic Estimation
Starting Assumptions
From our requirements conversation:
- 100 million new URLs created per day
- Read-to-write ratio: 100:1 (each URL is clicked ~100 times on average over its lifetime)
Write QPS (URL Creation)
100 million writes/day
÷ 86,400 seconds/day (round to 100,000 for mental math)
= ~1,000 QPS average
Peak (2-3x average): ~3,000 QPS
"So about 1,000 writes per second on average, peaking around 3,000. A single PostgreSQL instance handles 5,000-10,000 simple INSERT operations per second, so the write path is not our bottleneck. One database can handle this."
Read QPS (URL Redirection)
100 million URLs/day × 100 reads per URL (over lifetime)
But wait - the reads are spread over the URL's lifetime, not concentrated on creation day.
Let's estimate daily redirect traffic differently:
If we have 100M URLs created daily for 5 years:
Total URLs = 100M × 365 × 5 = ~182 billion URLs
If each URL gets clicked ~100 times total over 5 years:
Total clicks over 5 years = 182B × 100 = 18.2 trillion
But most clicks happen in the first few days (recency bias).
A more practical estimate: daily redirects ≈ 10 billion/day
10 billion / 100,000 = ~100,000 QPS average
Peak: ~300,000 QPS
flowchart TB
subgraph TRAFFIC["Traffic Summary"]
WRITE["Writes: ~1,000 QPS avg<br/>~3,000 QPS peak"]
READ["Reads: ~100,000 QPS avg<br/>~300,000 QPS peak"]
RATIO["Read:Write = 100:1"]
end
WRITE -->|"Single DB<br/>handles this"| W_ARCH["PostgreSQL primary<br/>is sufficient"]
READ -->|"Too high for DB alone"| R_ARCH["Redis cache required<br/>(100K ops/sec per instance)"]
RATIO -->|"Cache-first architecture"| DECISION["Serve 99%+ of reads<br/>from cache"]
Traffic summary for the URL shortener. Write QPS is about 1,000 average peaking at 3,000, which a single PostgreSQL instance can handle. Read QPS for redirects is about 100,000 average peaking at 300,000, far too high for a database alone. With a 100-to-1 read-to-write ratio, the architecture must be cache-first - serve 99% or more of reads from Redis cache, which handles 100,000 operations per second per instance.
Interview: How to present this: "We're looking at about 1,000 write QPS and 100,000 read QPS - a 100-to-1 read-heavy workload. The write path is manageable for a single database. The read path at 100K QPS needs a cache layer - Redis handles 100K ops/sec per instance, so a small Redis cluster with 3-4 nodes gives us plenty of headroom. This tells me the architecture is cache-first for reads, write-through to database."
Storage Estimation
URL Mapping Size
Each URL mapping record contains:
| Field | Size | Notes |
|---|---|---|
| Short code | 7 bytes | 7-character alphanumeric string |
| Long URL | ~200 bytes | Average URL length (can be up to 2,048 bytes) |
| Created timestamp | 8 bytes | Unix timestamp |
| Expiration timestamp | 8 bytes | When the URL expires |
| User ID (optional) | 8 bytes | If user accounts exist |
| Click count | 8 bytes | Basic analytics |
| Metadata overhead | ~60 bytes | Database row overhead, indexes |
Total per record: ~300 bytes (rounding up for safety)
Daily and Annual Storage
Daily: 100 million URLs × 300 bytes = 30 GB/day
Annual: 30 GB × 365 = ~11 TB/year
5 years: 11 TB × 5 = ~55 TB total
flowchart LR
DAILY["Daily Storage<br/>30 GB"] --> ANNUAL["Annual Storage<br/>~11 TB"]
ANNUAL --> FIVE["5-Year Storage<br/>~55 TB"]
FIVE --> DECISION{"Single DB?"}
DECISION -->|"55 TB exceeds<br/>single server"| SHARD["Will need sharding<br/>eventually"]
DECISION -->|"But 11 TB/year<br/>is manageable at start"| START["Start with single DB<br/>+ read replicas"]
Storage estimation. Each URL mapping is about 300 bytes. At 100 million URLs per day, that's 30 gigabytes per day, 11 terabytes per year, and 55 terabytes over 5 years. A single database server typically handles up to 5-10 terabytes comfortably. At 55 terabytes over 5 years, the system will need sharding eventually, but it can start with a single database plus read replicas for the first year or two.
Tip: The key insight to share: "At 55 TB over 5 years, a single database won't be enough long-term. But at 11 TB per year, I can start with a single PostgreSQL primary with read replicas and plan for sharding by year 2-3. I wouldn't over-engineer sharding from day one - that adds complexity I don't need yet."
Cache Estimation
Not all 182 billion URLs (5-year total) need to be in cache. The Pareto principle applies heavily: 20% of URLs get 80% of the clicks. And within that 20%, recent URLs get far more traffic than old ones.
Cache Strategy: Hot URLs Only
Cache the most frequently accessed URLs.
Daily active URLs (clicked at least once today): ~20% of recent URLs
Recent URLs (created in the last 30 days): 100M × 30 = 3 billion
Hot URLs (20% of recent): 3B × 0.2 = 600 million URLs
Cache size: 600 million × 300 bytes = ~180 GB
That's about 180 GB - which fits in a Redis Cluster with 3 nodes (each 64 GB, with replication).
Alternative: Cache Last 24 Hours of Created URLs
100 million URLs/day × 300 bytes = 30 GB
This is much simpler - just cache everything created in the last 24 hours.
Most clicks happen within the first day of creation.
30 GB fits in a single large Redis instance.
Interview: How to present cache estimation: "I'd cache the hot 20% of recent URLs - roughly 600 million entries at about 180 GB. That fits in a 3-node Redis Cluster. Alternatively, I could cache everything created in the last 24 hours at just 30 GB, since most clicks happen shortly after creation. Either approach keeps the cache hit rate above 95%, meaning only 5% of redirects hit the database. At 100K redirect QPS, that's 5K database reads per second - well within PostgreSQL's capacity."
Bandwidth Estimation
Read Bandwidth (Redirects)
100,000 redirect QPS × 300 bytes per response (short + long URL + headers)
= 30 MB/sec
= 240 Mbps
Peak: 300,000 QPS × 300 bytes = 90 MB/sec = 720 Mbps
This is comfortably within a single 1 Gbps network link. Bandwidth is not a concern for URL shortening - the payloads are tiny.
Write Bandwidth
1,000 QPS × 500 bytes (request includes long URL + metadata)
= 500 KB/sec = 4 Mbps
Negligible.
Short Code Space Estimation
This is a unique estimation for URL shorteners that interviewers specifically look for: how long should the short code be?
Characters Available
Using Base62 encoding (a-z, A-Z, 0-9): 62 characters
Codes Available by Length
| Code Length | Unique Codes | Years of Coverage (at 100M/day) |
|---|---|---|
| 5 | 62^5 = ~916 million | ~25 days |
| 6 | 62^6 = ~56.8 billion | ~1.5 years |
| 7 | 62^7 = ~3.5 trillion | ~96 years |
| 8 | 62^8 = ~218 trillion | ~5,970 years |
At 100 million URLs/day:
- Per year: 36.5 billion URLs
- 5 years: 182.5 billion URLs
- 10 years: 365 billion URLs
7-character Base62 gives us 3.5 trillion codes.
3.5 trillion / 365 billion (10 years) = ~9.6x headroom
7 characters is sufficient for decades.
flowchart LR
B62["Base62: a-z, A-Z, 0-9<br/>(62 characters)"]
LEN["Code length: 7"]
SPACE["3.5 trillion unique codes"]
NEED["Need: ~182 billion (5 years)"]
B62 --> LEN --> SPACE
NEED -->|"5% utilization<br/>at 5 years"| SPACE
NOTE["7 characters gives 96 years<br/>of runway at 100M URLs/day.<br/>Plenty of headroom."]
Short code space estimation. Using Base62 encoding with 62 characters, a 7-character code gives 3.5 trillion unique combinations. At 100 million URLs per day, that's 182 billion URLs over 5 years - only 5% of the available code space. Seven characters provides 96 years of runway, which is more than sufficient. Six characters would be tight at 56.8 billion codes - only 1.5 years before running out. Eight characters adds unnecessary length. Seven is the sweet spot.
Interview: What to say: "For the short code, I'd use 7 characters from Base62 - lowercase, uppercase, and digits. That gives us 3.5 trillion unique codes, which covers 96 years at 100 million URLs per day. We're only using about 5% of the code space over 5 years, so collision probability is extremely low. Six characters would be too tight - only 56 billion codes, about 1.5 years of runway. Seven characters is the right balance between URL brevity and code space."
The Complete Estimation Summary
Here's the one-page reference that drives all downstream design decisions:
| Metric | Value | Design Implication |
|---|---|---|
| Write QPS | 1,000 avg / 3,000 peak | Single DB primary handles this |
| Read QPS | 100,000 avg / 300,000 peak | Must use cache; DB alone can't serve this |
| Read:Write ratio | 100:1 | Cache-first architecture |
| Daily storage | 30 GB/day | Moderate growth |
| Annual storage | 11 TB/year | Single DB for year 1-2, shard after |
| 5-year storage | 55 TB | Sharding required |
| Cache size | 30 GB (24h) or 180 GB (hot 20%) | 1-3 Redis nodes |
| Bandwidth | 240 Mbps peak (reads) | Not a bottleneck |
| Short code length | 7 chars (Base62) | 3.5T codes, 96-year runway |
| Redirect latency target | <50ms p99 | Cache hit path must be <10ms |
Tip: The meta-lesson: Notice how each number connects to a design decision. 100K read QPS → need cache. 55 TB → need sharding. 7-character codes → 3.5T capacity. In your interview, don't just recite numbers - connect each number to the architectural choice it drives. That's what senior candidates do.
Key Takeaways
-
The read-write ratio is the most important number. 100:1 read-heavy tells you immediately that caching is critical. Without cache, 100K database reads per second requires an expensive distributed database. With cache, 95%+ of reads never touch the database.
-
Storage grows predictably - plan for sharding but don't start with it. 11 TB/year is manageable for a single database. 55 TB over 5 years requires sharding. Design with sharding in mind (choose a good shard key) but don't implement it until you need it.
-
7-character Base62 codes give you decades of runway. 3.5 trillion unique codes at 100M/day means 96 years before running out. This eliminates code exhaustion as a concern and keeps URLs short.
-
Bandwidth is not a bottleneck for URL shorteners. URL mappings are tiny (~300 bytes). Even at 300K peak QPS, total bandwidth is under 1 Gbps. Don't spend interview time discussing bandwidth optimization for this system.
-
Cache the hot subset, not the entire dataset. 55 TB of URL mappings won't fit in memory. But 30 GB of the last 24 hours' URLs or 180 GB of the hottest 20% gives you a 95%+ cache hit rate.
-
Every estimation should end with "and that tells me..." The number alone is useless. The design decision it drives is the whole point. Practice connecting numbers to decisions fluently.
Exercise: Exercise 1: Redo the estimation for a URL shortener that handles 1 billion URLs/day (10x our estimate - closer to a global platform). What changes? Does the short code length need to increase? Does the single-database write path still work? How many Redis nodes do you need for the read path?
Exercise 2: Estimate the storage and cache requirements if you add click analytics - tracking every click with timestamp, IP address, user agent, and referrer. Each click record is ~200 bytes. At 10 billion clicks/day, what's the daily and annual storage? Where do you store click data - same database as URL mappings, or a separate store? (Hint: write-heavy analytics data has very different access patterns from read-heavy URL mappings.)
Exercise 3: The interviewer asks: "What if 90% of clicks happen within the first hour after URL creation?" How does this change your caching strategy? (Hint: a 1-hour cache TTL might give you a higher hit rate than caching the last 24 hours, while using 1/24th the memory.)