Scaling Patterns

The standard moves, in the order you reach for them. Each step named with the bottleneck that forces it.

Vertical vs horizontal Statelessness The sequence Autoscaling Rate limiting
â†•ī¸

Vertical vs Horizontal

Vertical scaling buys a bigger machine — more CPU, RAM, faster disk. Horizontal scaling adds more machines and spreads load across them. The industry reflex is "scale horizontally," but vertical is underrated early: it's a config change, no distributed-systems complexity, and modern hardware is enormous.

âš ī¸
Vertical scaling has two hard limits. There's a ceiling — the biggest box money can buy — and a single machine is a single point of failure, no matter how large. Horizontal scaling removes both: near-limitless growth and redundancy built in. So vertical first for simplicity, horizontal when you hit the ceiling or need the redundancy. Both are legitimate; naming which limit you're solving for is the point.
🎈

Statelessness

Horizontal scaling is easy only if any server can handle any request. That requires statelessness: the server holds no per-user data between requests, so the load balancer can route freely and you can add or kill nodes at will. The instant a server remembers something a client depends on, you've pinned that client to that server and lost the freedom.

  MOVE STATE OFF THE APP SERVER:

     session data ─â–ļ Redis / signed JWT      (not local memory)
     uploaded files ─â–ļ object storage         (not local disk)
     local cache ─â–ļ treated as best-effort     (a miss is fine, never a source of truth)

  result: every app server is interchangeable → add/remove nodes freely,
          the load balancer routes anywhere

The work is pushing state out: sessions into Redis or a signed JWT (Security), files into object storage (Data Stores), and any local cache treated as a disposable optimization, never the truth. Do that and app servers become cattle — interchangeable, replaceable, scalable by the dozen.

đŸĒœ

The Scaling Sequence

Systems scale in a well-worn order. The value isn't memorizing it — it's naming the bottleneck that forces each step, because that's the reasoning an interviewer grades.

  1. One server (app + DB together)
        │  bottleneck: they compete for CPU/memory
  2. Split the database onto its own machine
        │  bottleneck: one app server maxes out
  3. Load balancer + N stateless app servers
        │  bottleneck: repeated reads hammer the DB
  4. Add a cache layer (Redis in front of the DB)
        │  bottleneck: read QPS still exceeds one DB
  5. Read replicas (spread reads across copies)
        │  bottleneck: slow work blocks request threads
  6. Move heavy work to async queues + workers
        │  bottleneck: one DB can't hold the data / write load
  7. Shard the database
MoveForced bySee
Separate DBApp and DB contend on one box—
LB + app serversOne app server saturatedDiscovery
Cache layerRepeated reads on the DBCaching
Read replicasRead QPS past one nodeReplication
Async queuesSlow work blocking threadsMessaging
ShardingWrite/storage past one nodeSharding
💡
Don't recite all seven steps at the start of a design. Reach for each one when your estimate shows the current design failing on a specific number (Estimation). "Read QPS from my estimate is 50K, past what one Postgres node serves, so I'd add read replicas here" is worth more than listing the whole ladder up front.
🔄

Autoscaling

Autoscaling adds and removes instances based on a metric — CPU, request rate, queue depth. Two realities make it trickier than "set a target and forget."

Warm-up lag scale-out is slow

A new instance isn't instant — it boots, pulls code, warms caches and connection pools. If you scale out only once you're saturated, the new capacity arrives after the spike hurt. Keep headroom: run above bare minimum so you can absorb a burst while new nodes warm.

Scale-in danger removing capacity

Scaling in too aggressively removes a node just before the next spike, or drops in-flight requests. Scale in slowly and conservatively — it's cheaper to run a little extra than to be caught short. Drain connections before terminating (Real-Time).

đŸšĻ

Rate Limiting & Throttling

Scaling handles legitimate load; rate limiting handles the rest — abusive clients, runaway scripts, retry storms. It caps how many requests a client can make in a window, enforced at the gateway so bad traffic dies before it reaches your services (Discovery). Four algorithms, differing mainly in how they handle bursts.

AlgorithmHow it worksBurst behavior
Token bucketTokens refill at a steady rate; each request spends oneAllows bursts up to bucket size
Leaky bucketRequests queue and drain at a fixed rateSmooths bursts into a steady flow
Fixed windowCount per clock window (per minute)Spike at window edges (double at the boundary)
Sliding windowRolling count over the last N secondsSmooth, no edge spike; more state
💡
Token bucket is the common default — it allows short bursts (good UX) while capping the sustained rate. Enforce at the gateway, return 429 with a Retry-After header so well-behaved clients back off (APIs). This is the heart of the Rate Limiter case study (Rate Limiter).
📋

Quick Reference

BottleneckMove
App + DB contendSeparate the database
App server saturatedLoad balancer + stateless app servers
Repeated reads on DBCache layer
Read QPS past one nodeRead replicas
Slow work blocks threadsAsync queues + workers
Write/storage past one nodeShard
Rate limitBurstBest for
Token bucketAllowed, cappedGeneral default, good UX
Leaky bucketSmoothed awaySteady downstream load
Fixed windowEdge spikeSimple, approximate
Sliding windowSmoothAccuracy, at more state cost
â„šī¸
The one-liners to keep ready. Vertical first for simplicity, horizontal when you hit the ceiling or need redundancy. Statelessness is what makes horizontal easy — push sessions, files, and cache off the app server. Reach for each scaling step only when a number forces it. Autoscale with headroom and scale in cautiously. Rate-limit at the gateway; token bucket is the sane default.