Frontend Labs
⋯

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)

Generated from real-life-system-design-guide.md · 8 Oct 2026 16:36