Contents
Course · 03
How do you share traffic across servers, and stop a flood?
A load balancer spreads requests over many servers. A rate limiter says "slow down" to anyone who asks too much. This page explains both, from the picture down to the real config, with interview drills at the end.
One front door (the load balancer) hides many servers, sends each request to a healthy one, and a rate limiter guards the door.
- L4 balancers move TCP connections without reading them. L7 balancers read the HTTP request, so they can route by path.
- Health checks remove sick servers. AWS ALB marks a target unhealthy after 2 failed checks, 30 s apart, by default.
- Token bucket allows short bursts. Sliding window counter is cheap and accurate: Cloudflare saw 0.003% wrong decisions.
- Keep the limiter's counter in one shared place (Redis), update it atomically, and answer
429withRetry-After.
—Words you need
Load balancing
01Why load balancers exist
One server has limits. It has a fixed CPU, memory and network. To serve more users, you add servers. This is called horizontal scaling (adding machines, not bigger machines).
But users need one address. The load balancer gives them one. It hides how many servers sit behind it. It also skips servers that are broken.
| Job | What it means | Without an LB |
|---|---|---|
| Spread load | Each server gets a fair share of requests. | One server melts, others sit idle. |
| Hide failures | Broken servers are skipped automatically. | Users hit the dead server and see errors. |
| Scale without users noticing | Add or remove servers behind the same name. | Every client needs a new address list. |
| Safe deploys | Drain a server, update it, put it back. | Deploys cause dropped requests. |
| One place for shared work | TLS, logs, limits, routing happen once. | Each server repeats the same work. |
The LB gives users one stable address and turns many fragile servers into one reliable service.
02L4 vs L7 load balancing
"Layer" comes from the OSI model, a 7-step picture of networking. Layer 4 is the transport layer (TCP, UDP). Layer 7 is the application layer (HTTP).
An L4 balancer picks a server per connection. It never reads the HTTP request. An L7 balancer reads the request first, then picks a server.
| Question | L4 (for example AWS NLB) | L7 (for example AWS ALB, NGINX) |
|---|---|---|
| What it sees | IP, port, protocol | URL path, host, headers, cookies, method |
| Routing by path | No | Yes |
| Speed and cost per request | Very low work per packet | More work: parse HTTP, often decrypt TLS |
| Protocols | Any TCP or UDP (databases, games, MQTT) | HTTP, HTTPS, HTTP/2, gRPC, WebSocket |
| Client IP at the server | Can be kept as is | Usually passed in X-Forwarded-For |
| Good for | Raw speed, static IPs, non-HTTP traffic | Microservices, path routing, auth at the edge |
Pick L7 when routing depends on the request. Pick L4 for raw speed, non-HTTP traffic, or a fixed IP.
Source: AWS: NLB introduction · AWS: ALB HTTPS listener
03Load balancing algorithms
The algorithm is the rule that picks a server. Each rule fits a different kind of traffic. Here is a worked example with three servers A, B, C.
| Algorithm | Rule | Worked example | Use it when | Watch out |
|---|---|---|---|---|
| Round robin | Take turns in order. | Requests go A, B, C, A, B, C. | Servers are the same size; requests cost about the same. | A slow request still counts as one turn. |
| Weighted round robin | Turns in proportion to a weight. | Weights 2:1:1 give A, A, B, C, A, A, B, C. | Servers have different sizes. | Weights are guesses; they go stale. |
| Least connections | Pick the server with fewest open connections. | A has 9, B has 2, C has 5. Next goes to B. | Requests vary a lot in length (uploads, WebSocket). | A new empty server gets flooded at once. |
| Least response time | Pick the server that answers fastest (often with fewest connections). | A averages 40 ms, B 120 ms. Next goes to A. | Servers differ in speed at run time. | Needs live timing data; can swing back and forth. |
| IP hash | hash(client IP) mod N picks the server. | Same client IP always lands on the same server. | Simple stickiness without cookies. | Many users behind one office IP all hit one server. Changing N moves most clients. |
| Consistent hashing | Servers and keys sit on a ring; a key goes to the next server clockwise. | Remove 1 of 4 servers: only that server's keys move (about a quarter). | Caches and shards, where moving keys is costly. | Needs virtual nodes to spread keys evenly. |
| Random of two choices | Pick 2 servers at random, send to the less loaded one. | Random picks B and C; C has fewer connections, so C. | Many balancers with no shared view of load. | Still needs a load signal per server. |
upstream api {
least_conn; # default is weighted round robin
server 10.0.0.1 weight=2;
server 10.0.0.2 max_fails=3 fail_timeout=10s;
server 10.0.0.3;
}
upstream cache_nodes {
hash $request_uri consistent; # ketama consistent hashing
server 10.0.1.1; server 10.0.1.2;
}
ip_hash uses "the first three octets of the client IPv4 address". hash ... consistent uses ketama, which "ensures that only a few keys will be remapped" when servers change. random two picks two servers and then uses least_conn by default. AWS ALB offers round robin (default), least outstanding requests and weighted random. HAProxy offers roundrobin, leastconn, source, uri, hdr, random and hash-type consistent. NGINX: upstream moduleDefault to round robin, switch to least connections when request lengths vary, and use consistent hashing when a key must keep landing on the same node.
Source: NGINX upstream module · AWS: ALB routing algorithm · HAProxy 2.8: balance · Karger et al. 1997, consistent hashing
04Health checks
A health check is a small test request. The balancer sends it to every server on a timer. Too many failures in a row means "take it out". Enough passes in a row means "put it back".
There are two kinds. Active checks send a test request, like GET /health. Passive checks watch real traffic and count errors.
| AWS ALB setting | Default | Meaning |
|---|---|---|
HealthCheckIntervalSeconds | 30 s | Time between checks of one target (instance or IP targets). |
HealthCheckTimeoutSeconds | 5 s | No answer in this time counts as a failure. |
UnhealthyThresholdCount | 2 | Failures in a row before the target is taken out. |
HealthyThresholdCount | 5 | Passes in a row before it is put back. |
Matcher | 200 | HTTP codes that count as healthy. |
Worked example: a server dies right after a check. Two more checks fail, 30 s apart. So users may hit it for up to about 60 s. Then it takes 5 passes, about 150 s, to come back.
max_fails defaults to 1 and fail_timeout to 10 s. As of both docs, checked 2026-10-25. AWS: ALB health checksMake health checks cheap and local. A deep check that tests shared parts can mark every server sick at once.
Source: AWS: ALB health checks · AWS: slow start · NGINX: server max_fails
05Sticky sessions and TLS termination
Sticky sessions
A sticky session pins a user to one server. The LB sets a cookie that names the server. Later requests with that cookie go to the same server.
Teams use it when the server keeps user state in memory, like a shopping cart. It feels easy. It causes problems later.
The fix is a stateless server. Keep sessions in a shared store, like Redis, or in a signed token, like a JWT. Then any server can serve any request.
TLS termination
TLS is the encryption in HTTPS. Terminating TLS means the LB decrypts it. Servers then get plain HTTP inside the private network.
| Mode | How | Good | Bad |
|---|---|---|---|
| Terminate at LB | LB holds the certificate and decrypts. | One place for certificates; LB can read HTTP and route. | Traffic inside is plain unless you re-encrypt. |
| Re-encrypt | LB decrypts, then opens a new TLS link to the server. | L7 features and encryption all the way. | Two TLS handshakes; servers need certificates too. |
| Passthrough | L4 LB forwards encrypted bytes untouched. | Only the server sees plain text. | No path routing, no header-based features at the LB. |
AWSALB; the load balancer generated cookie "has its own expiry of 7 days which is non-configurable", while stickiness duration is set from 1 second to 7 days. An ALB HTTPS listener uses a server certificate "to terminate the front-end connection and then decrypt requests". To keep traffic encrypted end to end without the LB decrypting, AWS points you to an NLB with a TCP listener on port 443. AWS: ALB HTTPS listenerTerminate TLS at the LB, keep servers stateless, and treat sticky sessions as a last resort.
06Forward proxy, reverse proxy, load balancer, API gateway
| Middleman | Who owns it | Main job | Example |
|---|---|---|---|
| Forward proxy | The client side | Filter or hide outgoing traffic, cache downloads | Office web proxy, Squid |
| Reverse proxy | The server side | Stand in front of servers: TLS, caching, compression | NGINX, HAProxy |
| Load balancer | The server side | A reverse proxy whose main job is spreading load | AWS ALB, NLB |
| API gateway | The server side | A reverse proxy for APIs: auth, rate limits, routing, aggregation | Amazon API Gateway, Kong |
/orders goes to the orders service, /users to users.429 Too Many Requests when limits are hit. RFC 9110 §3.7 IntermediariesEvery LB and API gateway is a reverse proxy. The difference is the main job: spreading load versus managing APIs.
Source: RFC 9110 §3.7 · AWS: API Gateway throttling · Azure: Gateway Aggregation pattern
07Global load balancing, and the LB as a single point of failure
| Method | How it picks | Failover speed | Example |
|---|---|---|---|
| GeoDNS | DNS answers with a different IP depending on where the user is. | Slow-ish: clients and resolvers cache DNS answers until the TTL ends. | Route 53 geolocation, latency, failover policies |
| Anycast | Same IP announced from many sites; internet routing (BGP) delivers to a near one. | Fast: routing moves away from a dead site; no DNS cache. | AWS Global Accelerator, Cloudflare |
The load balancer is itself a single point of failure
If all traffic goes through one LB box, that box can kill the site. So the LB is always built as a pair or a fleet.
| Setup | How it works | Good | Bad |
|---|---|---|---|
| Active-passive | One LB serves. A standby watches it with heartbeats. On failure the standby takes over the shared virtual IP (for example with VRRP / keepalived). | Simple. Easy to reason about. | Half the hardware sits idle. Failover takes seconds. |
| Active-active | Several LBs serve at once. DNS or anycast spreads clients across them. | No idle boxes. Losing one costs only part of capacity. | Must keep enough spare room for a loss; shared state (limits) is harder. |
| Managed cloud LB | The provider runs many nodes per zone and scales them for you. | No SPOF to manage yourself. | Less control; you trust the provider's limits. |
Use DNS or anycast to pick a region, a regional LB to pick a server, and always run the LB as more than one node.
Source: RFC 4786 anycast · AWS Global Accelerator · Route 53 routing policies · RFC 5798 VRRP
Rate limiting
08Token bucket and leaky bucket
Token bucket. Tokens drip into a bucket at a fixed rate. The bucket has a maximum size. Each request takes one token. No token, no entry.
Worked example: rate 2 per second, capacity 10. A quiet client saves up 10 tokens. It can then send 10 requests at once. After that it gets only 2 per second.
Leaky bucket. Requests pour into a queue. The queue drains at a fixed rate, like water from a hole. If the queue is full, new requests spill over and are dropped.
Worked example: drain 2 per second, queue size 5. A burst of 8 arrives. 5 wait in line, 3 are dropped. The 5 go out one every half second.
| Question | Token bucket | Leaky bucket |
|---|---|---|
| Bursts | Allowed, up to capacity | Smoothed into a steady stream |
| Output rate | Can spike briefly | Always steady |
| State per key | Token count + last refill time | Queue (or a "water level" + time) |
| Extra delay | None: allow or reject now | Requests may wait in the queue |
| Used by | Amazon API Gateway, Stripe | NGINX limit_req |
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
location /search/ {
limit_req zone=one burst=5 nodelay; # nodelay: serve burst now, reject past it
limit_req_status 429; # default is 503
}
limit_req uses "the 'leaky bucket' method"; its rejected requests get 503 unless you set limit_req_status. Stripe's 2017 post says they use "the token bucket algorithm" stored in Redis. AWS: API Gateway throttlingChoose token bucket for APIs, where short bursts are fine. Choose leaky bucket when the thing behind you needs a steady pace.
Source: AWS: API Gateway throttling · NGINX limit_req · Stripe: Scaling your API with rate limiters (2017)
09Fixed window, sliding log, sliding window counter
Fixed window. Keep one counter per minute, like user42:10:05. Reset it when the minute changes. Simple and cheap.
The flaw: the edges. Limit 100 per minute. A client sends 100 at 10:05:59 and 100 at 10:06:00. That is 200 in two seconds, and both windows say "fine".
Sliding window log. Store the time of every request. On each new request, drop times older than 60 s and count the rest. Exact, but it stores one entry per request.
Sliding window counter. Keep only two counters: last minute and this minute. Guess the last 60 s by weighting the old counter.
estimate = previous × (60 − 15) / 60 + current
= 42 × 0.75 + 18
= 49.5 # limit is 50, so this request is allowed
The guess assumes the old minute's requests were spread evenly. That is rarely exactly true, but it is close enough in practice.
| Algorithm | Memory per key | Accuracy | Allows bursts at edges | Best for |
|---|---|---|---|---|
| Fixed window | 1 counter | Low at edges | Yes, up to 2× the limit | Simple quotas (per day) |
| Sliding window log | 1 entry per request | Exact | No | Low volume, strict limits |
| Sliding window counter | 2 counters | Close (an estimate) | Mostly No | High volume APIs |
| Token bucket | 2 numbers | Exact for its model | Yes, by design (capacity) | APIs with fair bursts |
| Leaky bucket | Queue or 2 numbers | Exact for its model | No, smooths them | Protecting a steady backend |
The sliding window counter gives near-exact limits with just two numbers per key. It is the default answer for a high-volume API.
Source: Cloudflare blog, 2017
10Distributed rate limiting with Redis, HTTP 429, and where to put it
You run ten gateway copies. Each keeps its own counter in memory. A client spread over all ten gets ten times the limit. So the count must live in one shared place. Redis is the usual choice: it is in memory and runs each command one at a time.
Why atomic matters
The naive code is "read count, check, then add one". Two gateways can read 99 at the same moment. Both allow. Now 101 got in. This is a race condition (two actors interleave and break a rule).
Also, INCR then EXPIRE as two calls can leak. If the app crashes between them, the key never expires. The fix: run both inside one Lua script. Redis runs a script as one step.
local current = redis.call("INCR", KEYS[1]) -- KEYS[1] = "rl:k1:" .. minute
if current == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1]) -- ARGV[1] = 60 seconds
end
return current -- caller rejects if current > limit
ZREMRANGEBYSCORE rl:k1 0 (now_ms - 60000) # drop entries older than 60 s
ZADD rl:k1 now_ms request_id # record this request
ZCARD rl:k1 # how many in the last 60 s
PEXPIRE rl:k1 60000
What to send back: 429 and Retry-After
HTTP/1.1 429 Too Many Requests
Retry-After: 30
Content-Type: application/json
{"error":"rate_limited","limit":100,"window":"60s"}
Retry-After tells the client how long to wait. A good client waits that long, then retries with backoff and jitter (random extra delay).
Where to put the limiter
| Place | Good for | Weak spot |
|---|---|---|
| Edge / CDN / WAF | Floods and bots by IP, before they cost you anything | Does not know users or plans |
| API gateway | Per API key or per user limits, one place for all services | Needs a shared store for many copies |
| Inside each service | Costly endpoints, per-tenant fairness, concurrency caps | Every team must build it |
| Client SDK | Being polite: back off before the server says no | Cannot be trusted; never the only guard |
Retry-After itself is defined in RFC 9110 §10.2.3 as either an HTTP date or a number of seconds, for example Retry-After: 120. The Redis INCR page warns that INCR plus a separate EXPIRE has "a race condition" and fixes it with a Lua script. RFC 6585 §4Shared counter, atomic update, honest 429 with Retry-After. Put coarse limits at the edge and fine limits at the gateway.
Source: Redis INCR: rate limiter pattern · RFC 6585 §4 · RFC 9110 §10.2.3 · Stripe rate limiters
Practice
—When to use what
| Situation | Pick | Why |
|---|---|---|
| Microservices behind one domain, routed by path | L7 LB or API gateway | Only L7 can read /orders vs /users. |
| Game server, MQTT, database proxy, or need a static IP | L4 LB (NLB) | Non-HTTP traffic; per-connection routing; static IP per zone. |
| Same-size servers, short similar requests | Round robin | Simplest; fair enough. |
| Long and short requests mixed (uploads, WebSocket) | Least connections / least outstanding requests | Counts real work in progress, not turns. |
| Cache cluster or shards behind the LB | Consistent hashing | Adding a node moves only a small share of keys. |
| Server keeps session in memory and you cannot change it yet | Sticky sessions (for now) | Works today; plan to move state to Redis or tokens. |
| Users on several continents | GeoDNS or anycast in front of regional LBs | Lower latency; region failover. |
| Public API with fair short bursts | Token bucket | Allows bursts up to capacity, steady average. |
| Backend that must see a steady pace | Leaky bucket (queue) | Smooths spikes. |
| High-volume per-key limits, many gateway copies | Sliding window counter in Redis via Lua | Two numbers per key, atomic, shared. |
| Daily or monthly quota | Fixed window | Edge bursts do not matter at that scale. |
—Common mistakes and traps
—Interview questions
Show answer
X-Forwarded-For. Example: AWS ALB checks each target every 30 s by default and removes it after 2 failures. Follow-up: "What if the LB itself dies?" Run it as a pair or a managed fleet.Show answer
Show answer
Show answer
Show answer
GET /health, to each server on a timer. After N failures in a row it removes the server; after M passes it adds it back. ALB defaults: 30 s interval, 5 s timeout, 2 to fail, 5 to recover. What goes wrong: deep checks that call a shared DB can fail every server at once; slow detection lets users hit a dead server for about a minute; flapping servers go in and out. ALB fails open if all targets are unhealthy, sending traffic to all of them rather than none.Show answer
AWSALB. They exist because the server keeps session state in memory. Problems: load becomes uneven, new servers get little traffic, a server crash loses its users' sessions, and you cannot scale in until users leave. Better: make servers stateless. Keep sessions in Redis or in a signed token like a JWT, so any server can serve any request. Sticky can be fine for a short time, for example a WebSocket, which is sticky by nature.Show answer
Show answer
Show answer
Show answer
Show answer
Show answer
limit_req is leaky bucket; with burst=5 nodelay it serves a small burst at once and rejects the rest.Show answer
INCR per request. Fixes: sliding window log (exact but one entry per request) or sliding window counter (two counters, weighted). Cloudflare rejected fixed windows for exactly this edge spike.Show answer
Show answer
rl:{apiKey}:{window}. Do the check-and-increment atomically in a Lua script, so two gateways cannot both read 99 and both allow. Set the expiry in the same script; the Redis docs warn that a separate EXPIRE can leak keys. Shard keys over a Redis cluster by API key. Add a local in-memory pre-check for obvious abusers to save round trips. Decide fail open vs closed if Redis is down. Each check adds one Redis round trip to every request, so measure that latency and keep Redis close to the gateways.Show answer
429 Too Many Requests, defined in RFC 6585 §4. Include Retry-After (RFC 9110 §10.2.3), either seconds or a date, so clients know when to come back. Add a small JSON body with the limit and window. Many APIs also send headers with the limit and remaining count, but those are a convention, not one fixed standard. 429 responses "MUST NOT be stored by a cache". Clients should back off with jitter. Note: NGINX limit_req returns 503 by default; set limit_req_status 429.Show answer
Show answer
Show answer
Show answer
—Cheat sheet
| Fact | Value | Source |
|---|---|---|
| ALB health check interval / timeout | 30 s / 5 s (instance, IP targets) | AWS ALB health checks |
| ALB unhealthy / healthy threshold | 2 fails / 5 passes | AWS ALB health checks |
| ALB when all targets unhealthy | Fails open: routes to all | AWS ALB health checks |
| ALB deregistration delay | 300 s default | AWS ALB target group attributes |
| ALB algorithms | Round robin (default), least outstanding requests, weighted random | AWS ALB target group attributes |
| ALB sticky cookie | AWSALB, cookie expiry 7 days | AWS ALB sticky sessions |
| NLB TCP hash inputs | Protocol, src IP, src port, dst IP, dst port, TCP sequence | AWS NLB intro |
| NGINX default algorithm | Weighted round robin | NGINX upstream |
NGINX max_fails / fail_timeout | 1 / 10 s | NGINX upstream |
NGINX limit_req | Leaky bucket; rejects with 503 unless changed | NGINX limit_req |
| API Gateway throttling | Token bucket: rate = refill, burst = capacity | AWS API Gateway |
| Global Accelerator | 2 static anycast IPv4 addresses | AWS Global Accelerator |
| 429 Too Many Requests | RFC 6585 §4; must not be cached | RFC 6585 |
Retry-After | Seconds or HTTP date | RFC 9110 §10.2.3 |
| Sliding window counter | prev × (60 − t)/60 + current | Cloudflare 2017 |
| Its accuracy | 0.003% wrong of 400 million | Cloudflare 2017 |
| Fixed window worst case | About 2× the limit at an edge | Derived from the algorithm |
—Sources
| What | Where |
|---|---|
| ALB health check settings and fail-open | docs.aws.amazon.com: ALB target group health checks |
| ALB algorithms, stickiness, slow start, deregistration delay | docs.aws.amazon.com: ALB target group attributes |
| ALB TLS termination | docs.aws.amazon.com: ALB HTTPS listener |
| NLB layer 4 and flow hash | docs.aws.amazon.com: What is a Network Load Balancer? |
| NGINX algorithms, passive checks, sticky | nginx.org: ngx_http_upstream_module |
| NGINX leaky bucket limiter | nginx.org: ngx_http_limit_req_module |
| HAProxy balance algorithms | docs.haproxy.org: configuration, balance |
| Consistent hashing origin | Karger et al., STOC 1997 |
| Proxy vs gateway (reverse proxy) | RFC 9110 §3.7 |
| Retry-After | RFC 9110 §10.2.3 |
| 429 Too Many Requests | RFC 6585 §4 |
| Anycast definition | RFC 4786 |
| VRRP (active-passive virtual IP) | RFC 5798 |
| Anycast static IPs at the AWS edge | docs.aws.amazon.com: How Global Accelerator works |
| GeoDNS and failover routing | docs.aws.amazon.com: Route 53 routing policies |
| API Gateway token bucket and 429 | docs.aws.amazon.com: API Gateway throttling |
| Gateway aggregation pattern | learn.microsoft.com: Gateway Aggregation |
| Stripe's four limiters, token bucket in Redis | stripe.com/blog/rate-limiters (2017) |
| Sliding window counter formula and accuracy | blog.cloudflare.com (2017) |
| Redis INCR rate limiter pattern and race | redis.io: INCR |