AnyLearn
All lessons
Programmingintermediate

Scaling and Load Balancing

Master the mechanics of scaling distributed systems: when to scale up versus out, why statelessness is the prerequisite for horizontal scale, how L4 and L7 load balancers differ, which balancing algorithm fits which workload, and how to avoid turning your load balancer into the next single point of failure.

Not signed in — your progress and quiz score won't be saved.
Lesson progress1 / 8

Vertical vs horizontal scaling

Vertical scaling (scale-up): add CPU, RAM, or faster disks to one machine. Simple — no code changes, no coordination overhead. The ceiling is real: a 192-core AWS u-24tb1 costs ~$220/hr and still breaks at ~500k QPS on a single database. Horizontal scaling (scale-out): add more identical nodes. Theoretically unlimited, but requires your application to tolerate the new topology.

Real numbers to anchor intuition: a single c6g.4xlarge EC2 instance handles ~30k HTTP requests/sec. Add 10 of them behind a load balancer and you get ~300k req/sec at ~15% of the cost of one monster box. Scale-out wins on price-performance beyond ~4x vertical growth — which is why every large system eventually horizontalizes.

The hard prerequisite: your nodes must be stateless, or scaling adds more problems than it solves.

Full lesson text

All 8 steps on one page — for reading, reference, and search.

Show

1. Vertical vs horizontal scaling

Vertical scaling (scale-up): add CPU, RAM, or faster disks to one machine. Simple — no code changes, no coordination overhead. The ceiling is real: a 192-core AWS u-24tb1 costs ~$220/hr and still breaks at ~500k QPS on a single database. Horizontal scaling (scale-out): add more identical nodes. Theoretically unlimited, but requires your application to tolerate the new topology.

Real numbers to anchor intuition: a single c6g.4xlarge EC2 instance handles ~30k HTTP requests/sec. Add 10 of them behind a load balancer and you get ~300k req/sec at ~15% of the cost of one monster box. Scale-out wins on price-performance beyond ~4x vertical growth — which is why every large system eventually horizontalizes.

The hard prerequisite: your nodes must be stateless, or scaling adds more problems than it solves.

2. Stateless vs stateful services

A service is stateless if any node can handle any request with zero knowledge of prior interactions. Authentication tokens, request context, and session data live in the request itself (JWT) or in shared external storage (Redis, Postgres) — not in the server's local memory.

A stateful service remembers you. Classic failure mode: user logs in on Node A, next request routes to Node B, Node B has no session → user gets kicked out. Solutions in order of increasing complexity: sticky sessions (bind a client to one node), distributed session stores, or redesign to stateless.

Why it matters for scaling: stateless nodes are interchangeable. You can add 50 of them, kill any 10, and the load balancer adjusts without user impact. Sticky sessions destroy this property — they turn horizontal nodes into snowflakes. The design rule: push state out to Redis/Postgres; keep your app tier stateless.

3. L4 vs L7 load balancers

Layer 4 (transport-layer) LBs route based on IP and TCP/UDP port. They see no HTTP headers, no URLs, no cookies. Ultra-fast (kernel bypass, 10M+ connections/sec on AWS NLB) and TLS-agnostic — great for raw TCP throughput, databases, or protocols the LB shouldn't inspect.

Layer 7 (application-layer) LBs parse HTTP. They can route /api/* to one backend fleet and /static/* to another, insert X-Forwarded-For, terminate TLS, inspect cookies for sticky sessions, and return a 503 before a bad request hits your app. Latency overhead: ~0.5–2 ms. Worth it for most web workloads.

FeatureL4 (NLB)L7 (ALB/Nginx)
Routing basisIP + portURL, headers, cookies
TLS terminationPass-throughYes
Throughput10M+ conn/sec~1M req/sec
HTTP-awareNoYes
CostLowerHigher

NGINX and HAProxy are the dominant open-source L7 choices; AWS ALB and GCP HTTPS LB are managed equivalents.

4. Balancing algorithms

Round-robin: requests cycle through nodes in order. Zero memory, works when nodes are homogeneous. Breaks badly if request cost varies wildly (one slow DB query holds a slot).

Least connections: route to the node with the fewest active connections. Better for heterogeneous load — long-running requests don't starve new ones. ~2–5% CPU overhead at the LB.

IP hash / consistent hashing: hash the client IP (or a session key) to a node. Provides soft stickiness without explicit session state at the LB. Consistent hashing with virtual nodes (used in Cassandra, Nginx upstream) minimizes reshuffling when nodes join/leave — only ~1/N keys remap, not all of them.

upstream backend {
    consistent_hash $request_uri;
    server app1:8080;
    server app2:8080;
    server app3:8080;
}

Weighted round-robin: assign weight proportional to node capacity. Useful during blue-green deploys (10% weight on new version = canary).

5. Scalable architecture overview

Traffic path from client to auto-scaled app tier, with health checks and shared state.

flowchart LR
  C["Client"]
  DNS["DNS (anycast / GeoDNS)"]
  LB1["L7 Load Balancer (active)"]
  LB2["L7 Load Balancer (standby)"]
  A1["App Node 1"]
  A2["App Node 2"]
  A3["App Node 3"]
  AS["Autoscaler"]
  R["Redis (session / cache)"]
  DB["Primary DB"]
  C --> DNS
  DNS --> LB1
  LB1 --> LB2
  LB1 --> A1
  LB1 --> A2
  LB1 --> A3
  AS --> A3
  A1 --> R
  A2 --> R
  A3 --> R
  A1 --> DB
  A2 --> DB
  A3 --> DB

6. Health checks and failure detection

A load balancer without health checks routes to dead nodes — guaranteed 5xx storms. Every serious LB implementation runs two kinds:

Active checks: the LB polls GET /healthz every 5–10 seconds. A node is marked unhealthy after N consecutive failures (typically 2–3) and re-enabled after M consecutive successes (typically 2). Tune aggressively — 10-second detection + 20-second recovery = 30 seconds of 5xx traffic.

Passive checks (circuit breaking): the LB watches real request error rates. NGINX upstream supports max_fails=3 fail_timeout=30s. HAProxy tracks rolling error percentages. AWS ALB uses target group health thresholds.

Gotcha: a /healthz endpoint that returns 200 while the app is deadlocked is worse than useless. Make the health check actually exercise a DB query or Redis ping. Budget < 100 ms for the check to complete, so it doesn't itself become a bottleneck.

7. Autoscaling

Autoscaling turns capacity from a fixed cost into a variable one. The two models:

Reactive autoscaling: trigger on CPU > 70% or request queue depth > 500. AWS Auto Scaling Groups, GCP Managed Instance Groups, and Kubernetes HPA all support this. Lag problem: spinning up a new EC2 instance takes 90–120 seconds — if your traffic spike lasts 60 seconds, reactive scaling never fires in time.

Predictive autoscaling: train on historical traffic patterns (Monday 9 AM always spikes) and pre-warm capacity. AWS predictive scaling, GCP Scheduled Autoscaler. Reduces cold-start lag to near zero for predictable workloads.

Scale-in is dangerous: terminate a node mid-request and you drop traffic. Always use connection draining (graceful shutdown) — wait for in-flight requests to complete (typically 30 s timeout) before killing the instance. Kubernetes does this via terminationGracePeriodSeconds.

8. Avoiding the single point of failure trap

Here's the irony: you scale your app to 20 nodes, then route all traffic through one load balancer. That LB is now your SPOF.

Active-passive LB pairs: one LB is primary; the standby takes a VIP (virtual IP) via VRRP/keepalived if the primary dies. Failover: 5–15 seconds. Common with bare-metal Nginx or HAProxy.

Active-active LB pairs: both LBs handle traffic simultaneously. DNS round-robins between them. Failure removes one; traffic routes to the survivor with no failover delay.

Managed cloud LBs (AWS ALB, GCP LB) are regionally redundant by design — the SPOF risk shifts to your configuration. But a misconfigured security group that blocks the health check port has the same effect as a dead LB.

Design rule: anything in the critical path of 100% of your requests must be HA. Draw the traffic flow end-to-end; circle every single hop; ask "what breaks if this dies?"

Check your understanding

The lesson ends with a 5-question quiz. Take it in the player above to see your score.

  1. You have a stateful Node.js app that stores session data in local memory. You scale it to 10 nodes behind a round-robin load balancer. What is the most likely symptom?
    • Throughput increases by exactly 10x with no issues.
    • Users are randomly logged out because their session lives on a different node than the one handling their next request.
    • The load balancer becomes the bottleneck and limits throughput to a single node's capacity.
    • Memory usage drops by 10x because sessions are distributed across nodes.
  2. Which load-balancing algorithm minimizes reshuffling of requests when a node is added to or removed from the pool?
    • Round-robin, because it distributes evenly by definition.
    • Least connections, because it always picks the least-loaded node.
    • Consistent hashing with virtual nodes, because only ~1/N keys remap.
    • IP hash, because the client IP never changes.
  3. Your L7 load balancer's health check hits GET /healthz and always gets a 200 OK, but users are seeing 503 errors. What is the most likely root cause?
    • The health check interval is too short.
    • The /healthz handler returns 200 without actually verifying that the database or downstream dependencies are reachable.
    • L7 load balancers cannot perform health checks — only L4 can.
    • Round-robin routing is sending requests to the wrong port.
  4. Reactive autoscaling on CPU > 70% fires, but your traffic spike is over before the new instances are healthy. What is the right fix?
    • Lower the CPU threshold to 50% so scaling fires sooner.
    • Switch to predictive autoscaling that pre-warms capacity based on historical traffic patterns.
    • Increase instance size so CPU stays below 70% during spikes.
    • Add more load balancers to spread the traffic across existing nodes faster.
  5. What is the key operational difference between L4 and L7 load balancers?
    • L4 can terminate TLS; L7 cannot.
    • L7 routes by URL path and HTTP headers; L4 routes by IP address and port only.
    • L4 supports health checks; L7 does not.
    • L7 is faster than L4 because it uses kernel bypass networking.

Related lessons