"Scale" gets talked about as one bigger number on a graph. It isn't.
Traffic finds whichever layer has the smallest ceiling, and each order
of magnitude hits a different one. Follow one endpoint, GET /products/:id, from 1k to 1M requests per
second and ask the same question at each step: with the same code, what gives first?
The one formula
Little's law: requests in flight = arrival rate × time per request. An 8ms query at 1k req/s means 8 queries in flight at any instant. At 10k, it's 80.
That's why a system can look completely fine and then fall over at one specific number. Here's the map; the rest of the post walks it.
| Tier | What actually breaks first | What fixes it |
|---|---|---|
| 1k req/s | Nothing yet, that's the trap | Nothing, but don't mistake this for validation |
| 10k req/s | Postgres connection ceiling; single instance's own throughput | Shared cache, a pooler, and per-instance state made shared |
| 100k req/s | Cache node throughput; cache stampede on hot keys; LB TLS termination | Request coalescing, more cache/LB nodes, keep-alive |
| 1M req/s | Correlated retries from clients you don't control | Circuit breakers, load shedding, cell-based isolation |
1k: everything works, which is the trap

Eight queries in flight against a pool of 20 and a
#postgres limit of
100.
Nothing notices. Neither would an N+1 query, a rate limiter counting
in a plain Map, or a cache in module memory. At 1k req/s on one
instance, all of it works. That isn't validation; it's cover.
10k: the database and the single instance
Eighty connections for one query on one endpoint, and the service has twelve endpoints, each with its own pool of 20 sized in isolation. Under real traffic they're busy together: 240 possible connections against 100. Requests queue for a connection, and every endpoint slows at once for a reason none of them caused.
The other wall: one Node event loop can't push 10k real requests a second. So you run ten instances behind a load balancer, and anything that assumed one process quietly breaks. A per-instance rate limit is now ten times too generous; a per-instance cache has a tenth of the hit rate. Same fix for both problems: stop treating per-instance state as global, and stop asking Postgres for more connections than it has.

- App instance to Redis cache: cache-aside read
- App instance to Redis cache
- App instance to Redis cache
- Redis cache to PgBouncer: cache miss
- PgBouncer to Postgres primary: fixed real connections
#redis is one shared cache for every instance; PgBouncer caps the real connections no matter how many instances open pools. (The full pool-sizing story is in Designing Multi-Tenant APIs That Scale.)
100k: the cache and the load balancer
The fix from the last tier is now the system under test. Redis
runs commands one at a time on one core,
and a plain GET tops out around 100k to 200k ops a second. One hot
endpoint is already near that, competing with everything else on the
node.
Volume also makes a rare coincidence routine. A popular product's cache entry expires, and in the next few milliseconds thousands of requests for it all miss and all query Postgres for the same row: a cache stampede. The database didn't get slower; a TTL expired at a busy moment. The fix is coalescing: the first miss takes a short lock and refills the key, everyone else waits for that one fetch.

(Cloudflare's probabilistic early revalidation avoids the hard expiry altogether.) The load balancer stops being free too: terminating TLS is CPU per connection. Keep-alive amortises the handshake, and more than one balancer node removes the single ceiling, the same move as before.
1M: the failure mode changes
Every fix so far had one shape: a resource has one real ceiling, so share it and add more of it. At 1M req/s the most likely outage isn't a ceiling. It's a million clients all reacting to the same small blip in the same way.
- Auth slows to 2s to Timeouts: calls time out
- Timeouts to Retry flood: every client retries
- Retry flood to Still slow: more load than normal
- Still slow to Auth slows to 2s: long after the spike
Backoff with jitter spreads the retries out, but it doesn't reduce how many there are: a million clients hammering a struggling service over five seconds instead of one. Two more pieces break the loop. A circuit breaker removes the calls: after enough failures, every call fails locally and instantly, and nothing reaches the dependency.

Load shedding handles what you can't serve: reject it early and
cheaply, before it spends the capacity the requests you can serve
need. An honest 503 with Retry-After beats a slow failure that
looked like it was trying.

And at this volume something is always broken somewhere, so the last move is containing it. Split traffic into independent cells, each with its own instances, cache and database shard:
- Edge / load balancer to Cell 1: app + cache + DB shard: shard key A-H
- Edge / load balancer to Cell 2: app + cache + DB shard: shard key I-P
- Edge / load balancer to Cell 3: app + cache + DB shard: shard key Q-Z
A bad deploy or a hot key then costs one cell's slice, not the whole million. Shuffle-sharding takes it further by giving each customer an overlapping subset of cells, so even a dead cell touches a small fraction of anyone's traffic.
Same story, different resource
Every tier is the same story with a different resource: something has exactly one real capacity, more than one caller assumes it has more, and the gap only shows once traffic is high enough. At 1k the gap is invisible. At 1M it includes everyone else's reaction to the last failure, which is why the fix stops being "add capacity" and becomes "limit what one failure can reach".
If this is happening in production right now, the redesign can wait: start with the first 15 minutes of a prod incident.