4. Building Blocks & Scaling from 1 to 10 Million Users
4.1 The canonical web stack
┌───────────┐
Users ──DNS──────────▶│ CDN │── static assets, cached API responses
│ └─────┬─────┘
│ ▼
│ ┌──────────────────┐
└──────────────▶│ L7 Load Balancer │ TLS termination, routing, WAF
│ / API Gateway │ authN, rate limiting
└────────┬─────────┘
▼
┌─────────────────────────────┐
│ Stateless app servers (N) │ autoscaled
└──┬───────────┬──────────┬───┘
▼ ▼ ▼
┌────────┐ ┌──────────┐ ┌───────────┐
│ Cache │ │ Primary │ │ Queue / │──▶ async workers
│(Redis) │ │ DB │ │ Stream │
└────────┘ └────┬─────┘ └───────────┘
│ replication
┌────▼─────┐ ┌───────────────┐
│ Replicas │ │ Object storage│ (blobs, media,
└──────────┘ │ (S3/GCS/Blob)│ backups)
└───────────────┘
| Component |
Job |
Key decisions |
| DNS |
Name → IP; can do geo routing and failover |
TTL (low TTL = faster failover, more queries) |
| CDN |
Serve static/cacheable content from the edge |
Cache keys, TTL, purge; see the CDN guide |
| Load balancer (L4) |
Distribute TCP/UDP connections |
Fast, protocol-agnostic, no HTTP awareness |
| Load balancer (L7) |
Route by HTTP path/header, terminate TLS |
Smarter routing, more CPU; algorithms: round robin, least connections, consistent hash, power-of-two-choices |
| API gateway |
AuthN, rate limits, request shaping, routing to services |
Becomes a critical shared dependency; keep it thin |
| App servers |
Business logic |
Stateless, so any instance can serve any request |
| Cache |
Serve hot reads from memory |
Pattern, TTL, invalidation (§7) |
| Database |
Durable source of truth |
Model, consistency, scaling path (§6, §9, §10) |
| Queue / stream |
Decouple, buffer, async work |
Delivery semantics, ordering (§11) |
| Object storage |
Cheap, durable blobs |
Lifecycle tiers, presigned URLs |
4.2 The scaling story (a common opening interview prompt)
| Stage |
Users |
What you add |
Why |
| 1 |
0–1k |
One server running app + DB |
Simplest thing that works |
| 2 |
1k–10k |
Separate the DB onto its own machine |
Scale tiers independently; DB needs different hardware |
| 3 |
10k–100k |
LB + 2+ stateless app servers; sessions in Redis or a JWT |
Redundancy; horizontal scale |
| 4 |
100k–1M |
Read replicas, cache tier, CDN for static assets |
Reads dominate; cut DB load by 80–95% |
| 5 |
1M–5M |
Queues for async work (email, image resize, fan-out) |
Keep the request path fast; absorb bursts |
| 6 |
5M–10M+ |
Shard the write path; split services along team/domain lines |
Single primary is now the write bottleneck |
| 7 |
10M+ |
Multi-region, cells, dedicated data platform |
Latency for global users, blast radius, DR |
Senior nuance: don't skip stages. A common, expensive mistake is
starting at stage 6 (microservices plus sharding) with 10k users. A single
well-tuned Postgres instance handles far more than most startups ever reach;
Stack Overflow famously ran on a small number of SQL Server machines for
years.
4.3 Load-balancing algorithms
| Algorithm |
Good for |
Watch out |
| Round robin |
Homogeneous, short requests |
Ignores current load |
| Least connections / least outstanding requests |
Variable request durations |
Needs state per backend |
| Power of two choices (pick 2 at random, send to the less loaded one) |
Large fleets; near-optimal with little coordination |
— the default to suggest |
| Consistent hashing |
Sticky routing for cache locality (e.g. per-user shards) |
Hot keys; use bounded loads |
| Weighted |
Mixed hardware, canaries |
Needs weight management |
4.4 🏗️ Flow: lifecycle of one request through the stack
Browser
│ 1. DNS: resolver → authoritative (GeoDNS / latency-based) → returns anycast VIP
│ 2. TCP + TLS 1.3 to nearest edge PoP (anycast routes to closest)
▼
Edge / CDN PoP ──cache hit?──yes──▶ respond (≈10-30 ms)
│ no (dynamic API)
│ 3. Edge → origin over warm, pooled connections (often a private backbone)
▼
L4 load balancer (ECMP + consistent hashing on 5-tuple, e.g. Maglev-style)
│ 4. picks an L7 proxy; connection-level, no TLS termination (or TLS passthrough)
▼
L7 proxy / API gateway (Envoy/NGINX): TLS termination, authN, rate limit, routing
│ 5. route /v1/orders → orders service; picks instance via least-request / P2C
▼
Service instance: deadline = 800 ms remaining
│ 6. cache GET (0.5 ms) → miss → DB query (3 ms) → cache SET
│ 7. async: emit event to Kafka (does not block response)
▼
Response flows back; access log + trace span emitted at every hop
4.5 Deeper: load balancers, health checks & connection management
- L4 load balancing at scale: Google's Maglev (NSDI 2016) runs
software L4 balancers on commodity machines behind ECMP routers. It uses
consistent hashing so that any Maglev machine sends a given connection
to the same backend, and adding or removing a balancer doesn't break
existing connections. Cloud network LBs are built on the same idea.
- Direct Server Return (DSR): responses bypass the LB and go straight to
the client. This helps when responses are much larger than requests (video, downloads).
- L7 at scale: Lyft built Envoy (open-sourced 2016) as a sidecar and
edge proxy. It became the data plane for most service meshes. It brought
outlier detection (eject hosts with consecutive 5xx), retry budgets, and
zone-aware routing into one place.
- Health checks have two jobs: liveness (restart me) and readiness
(send me traffic). Never make readiness depend on a shared dependency
such as the DB. If the DB blips, every instance reports unready at once and
the LB has nowhere to send traffic. Good LBs fail open: when all hosts
are unhealthy, route to all of them.
- Slow start: new instances (cold JIT, empty caches) should ramp their
share of traffic over 30–120 s. Least-request balancing otherwise floods
the "idle" new instance.
4.6 🌍 In production
- Stack Overflow served a top-50 website from a small number of on-prem
SQL Server and web servers for years, publicly documented on Nick Craver's
blog. Vertical scaling plus heavy caching goes a long way.
- Instagram reached tens of millions of users on Django + Postgres +
Memcached + Redis with a very small team, sharding Postgres by hand.
- Shopify kept a modular monolith (Rails), enforcing component
boundaries with tooling (Packwerk) rather than splitting into hundreds of services.
- Google Maglev and Lyft Envoy are the canonical L4 and L7 designs to
name in an interview.
4.7 ⚠️ Corner cases & deep details
| Corner case |
What happens |
Fix |
| Long-lived HTTP/2 / gRPC connections |
After scale-out, new instances get no traffic, because clients multiplex everything over existing connections to old instances |
L7 (per-request) balancing, client-side LB with periodic re-resolution, or a max connection age on the server so clients reconnect |
| LB idle timeout > backend keep-alive timeout |
The backend closes an idle connection just as the LB sends a request on it, causing sporadic 502s under low load |
Backend keep-alive timeout must be longer than the LB's idle timeout |
| Clients ignore DNS TTL |
Failover by DNS takes hours for some clients (older JVMs cached DNS forever by default, and many SDKs pin IPs) |
Configure client DNS TTLs; prefer anycast or LB-level failover over DNS changes |
| Graceful shutdown missing |
Each deploy drops in-flight requests, causing error spikes on every rollout |
Fail the readiness check → wait for the LB to deregister (≥ health-check interval) → drain in-flight requests → exit. In Kubernetes, use a preStop sleep. |
| Sticky sessions |
One instance accumulates heavy users, gets hot, and loses all sessions when it dies |
Externalize sessions; avoid stickiness except for cache locality, and then use consistent hashing with bounded load |
| Autoscaling lag |
A spike arrives in 30 s, but new capacity takes 3–5 minutes (boot, image pull, warm-up) |
Scale on leading indicators (queue depth, RPS), keep warm pools, pre-scale for known events |
| Retry at the LB plus retry in the client |
A single failure becomes 9 attempts |
Retry at one layer only, and only for idempotent requests (§14) |