0The wall
Chirp launched on one server: an app process and a PostgreSQL database on the same box. It was fine at 10k users. At 40M users and peak traffic, that design would need to serve ~1,000,000 requests/sec — and it fell over long before. The question isn't "how do we make the server faster" — it's "where is the bottleneck, and what do we do when it moves?"
🧠 The one idea behind every rung of the ladder
Every layer of a system is either stateless or stateful, and they scale in opposite ways:
- Stateless layers (CDN, load balancer, app servers) remember nothing between requests, so any copy can serve anyone. You scale them by cloning — add identical boxes behind a load balancer. This is easy, and it's exactly what Kubernetes and autoscaling automate.
- Stateful layers (the database) hold the shared truth, so you can't just run 50 independent copies that all disagree. This is the hard part.
So a flood of requests fans out cheaply across the cloneable layers and then funnels onto the one thing you can't trivially clone: Postgres. That's why nearly every scaling technique below — replicas, cache, sharding, queues — is really about protecting or splitting the stateful layer. Scale the easy stuff first; then spend your real effort on the database.
1Read the traffic before you scale it
You can't scale what you haven't measured. Chirp's traffic is overwhelmingly reads (people scroll far more than they post), and it's fan-out heavy (one celebrity post reaches millions of timelines). That shape dictates the whole strategy: optimize reads hard, make writes asynchronous.
| Request | Share of traffic | Read/Write | Latency budget | Scaling lever |
|---|---|---|---|---|
| Load timeline | ~80% | read | < 100ms | cache + read replicas |
| Post a chirp | ~5% | write | < 300ms (async fan-out) | queue + outbox |
| Notifications / counts | ~10% | read | < 200ms | cache, precomputed |
| Search / profile | ~5% | read | < 300ms | dedicated index/store |
Insight 1
Insight 2
Insight 3
2The architecture, three ways
Here's the shape Chirp grew into — the same system as an ASCII request path, an entity diagram of the sharded data model, and a sequence diagram of one timeline read.
1,000,000 req/s
│
▼ CDN / edge cache ........ static + hot timelines (absorbs a huge %)
▼ Load balancer ........... spreads what's left
▼ [ app ][ app ][ app ]×N . STATELESS — just add clones (autoscale)
▼ Redis cache ............. timelines & counts (most reads stop here)
│ │ miss
▼ ▼
write path read replicas ×M .... scale READS by cloning the data
(queue → │
async ▼
fan-out) PRIMARY (sharded) .... the stateful WALL — split by user_id
shard0 shard1 shard2 ... shardK
easy to clone ↑ ↑ the hard part: one source of truth per shard3The scaling ladder
Chirp climbed one rung at a time, and only when a real bottleneck forced it — because every rung adds complexity you'll debug at 3am. Each stage below is a symptom, the move, and the trap. Expand them.
1Scale up — one bigger box▶
Symptom: CPU/RAM maxed on the single server; latency creeping up.
The first and most underrated move: give the box more CPU, RAM, and faster disk. Vertical scaling has no distributed-systems complexity — no consistency, no coordination — and it buys real headroom fast. Do this before anything clever.
2Split the app tier — stateless clones behind a load balancer▶
Symptom: the app process is the bottleneck (CPU-bound), or you need zero-downtime deploys and failover.
Move the database off the app box, then run many identical app servers behind a load balancer. Because the app is stateless (no session state on the box — put sessions in Redis/tokens), any clone serves any request, and autoscaling adds/removes them with load. This is the cheap, near-infinite part of scaling.
3Read replicas — scale reads by cloning the data▶
Symptom: the database is read-saturated; 80% of load is timelines and the primary can't keep up.
Add read replicas — copies of the database that stream changes from the primary and serve read-only queries. Route reads to replicas, writes to the primary. Since Chirp is read-heavy, this multiplies read capacity by adding replicas — cloning, applied to (mostly-read) data.
4Cache — stop most reads before they reach the DB▶
Symptom: even with replicas, the same hot timelines are read millions of times; the DB does redundant work.
Put Redis in front of the database. Cache assembled timelines, counts, and hot objects with a TTL. At a ~90% hit rate, nine of ten reads never touch Postgres — the cheapest scaling you can buy. Combined with the edge/CDN, most reads end far from the stateful layer.
5Shard — split the stateful layer itself▶
Symptom: writes and total data outgrow a single primary; one Postgres can't hold or write it all, even with replicas (replicas scale reads, not writes/size).
Split the data across shards — independent databases, each owning a slice of users by user_id. Now writes and storage scale horizontally: shard 3 knows nothing about shard 7. This is the real answer to a stateful bottleneck — you stop cloning the whole thing and start partitioning it.
user_id), and resharding later is a major migration — plan capacity ahead.6Go async — queues, fan-out, and the outbox▶
Symptom: a single post must write to millions of follower timelines; doing that synchronously makes posting slow and fragile.
Take heavy work off the request path. A post writes the chirp and drops a job on a queue (Kafka/SQS); background workers fan it out to follower timelines. The user's post returns in milliseconds; the fan-out happens behind the scenes. Use the outbox pattern so the write and the event are atomic, and design consumers to be idempotent (the same fan-out job may run twice).
4What breaks at a million per second
The rungs get you the throughput; these are the failure modes that show up because you scaled, and the teams that survive them planned for them.
THUNDERING HERD a hot cache key expires → thousands rebuild it at once
fix: TTL jitter + single-flight rebuild lock
HOT SHARD one celebrity / one popular key overloads a single shard
fix: hybrid fan-out (push normal, pull celebrity); split hot keys
REPLICATION LAG replica behind → stale reads, broken "read-your-writes"
fix: read from primary after a write; alarm on lag vs budget
RETRY STORM a blip → everyone retries → self-inflicted DDoS
fix: backoff + jitter, circuit breakers, load shedding (429)5Scorecard — climb a rung only when it's forced
The senior skill isn't knowing these techniques — it's knowing when not to reach for them yet. Each rung is complexity you'll debug forever. Add one only when the box below it is genuinely the bottleneck.