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.

TL;DR

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 429 with Retry-After.
30s
Default ALB health check interval (instance targets)
2fails
Failed checks in a row before ALB marks a target unhealthy
0.003%
Requests wrongly allowed or blocked by Cloudflare's sliding window (400 million tested)
429
HTTP status for "Too Many Requests" (RFC 6585)

—Words you need

Each jargon word once, in plain words glossary

Load balancer (LB)
A traffic cop that sends each request to one of many servers.
Target / backend
One server behind the load balancer that does the real work.
Layer 4 (L4)
Works with IP addresses and ports only. Does not read HTTP.
Layer 7 (L7)
Reads the HTTP request: path, headers, cookies.
Health check
A small test request the LB sends to see if a server works.
Sticky session
Sending the same user to the same server every time.
TLS termination
The LB decrypts HTTPS, so servers get plain HTTP.
Reverse proxy
A middleman that stands in front of servers and speaks for them.
API gateway
A reverse proxy that also checks logins, limits, and routes APIs.
Anycast
One IP address announced from many places; the nearest answers.
SPOF
Single point of failure: one part whose death stops everything.
Rate limit
A cap on how many requests one caller may send per time.
Atomic
Done as one step; nobody can slip in halfway.
Lua script (Redis)
A tiny program Redis runs as one atomic step.

Load balancing

01Why load balancers exist

One address outside, many servers inside core

A load balancer in front of three servers Users send HTTPS requests to one load balancer. It forwards them to Server A and Server B, and sends nothing to Server C, which failed its health check. HTTPS Usersbrowsers, apps Load balancerone public name Server Ahealthy Server Bhealthy Server Cfailed check LEGEND Users Balancer Healthy Out of rotation
The dashed line goes to a server that failed its check. It gets no traffic until it passes again.

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.

JobWhat it meansWithout an LB
Spread loadEach server gets a fair share of requests.One server melts, others sit idle.
Hide failuresBroken servers are skipped automatically.Users hit the dead server and see errors.
Scale without users noticingAdd or remove servers behind the same name.Every client needs a new address list.
Safe deploysDrain a server, update it, put it back.Deploys cause dropped requests.
One place for shared workTLS, logs, limits, routing happen once.Each server repeats the same work.
In everyday words
A supermarket has one entrance and ten tills. A staff member at the front points each shopper to a free till. If a till breaks, she stops sending people there.
Actually
AWS ALB runs one load balancer node per enabled Availability Zone. Each node checks every target and "routes requests only to the healthy targets". When a target is deregistered, ALB waits 300 s by default (deregistration delay) so in-flight requests can finish. As of the ALB docs, checked 2026-10-25. AWS: ALB target group attributes

The LB gives users one stable address and turns many fragile servers into one reliable service.

Source: AWS: ALB health checks · AWS: deregistration delay

02L4 vs L7 load balancing

Does the balancer read the letter, or only the envelope? interview favourite

Layer 4 versus layer 7 load balancing The top row shows a layer 4 balancer that sees only IP and port and passes packets to any server. The bottom row shows a layer 7 balancer that reads the URL and sends /api requests to the API pool and /img requests to the image pool. LAYER 4 · TCP / UDP LAYER 7 · HTTP TCP PACKETS HTTPS /API/* /IMG/* Clientany protocol L4 balancersees IP + port Server poolany server Clientbrowser L7 balancerreads URL, headers API poolapp servers Image poolmedia servers LEGEND Client Blind balancer Reads request Server pool
L4 sees only the envelope. L7 opens the letter, so it can route by what is inside.

"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.

QuestionL4 (for example AWS NLB)L7 (for example AWS ALB, NGINX)
What it seesIP, port, protocolURL path, host, headers, cookies, method
Routing by pathNoYes
Speed and cost per requestVery low work per packetMore work: parse HTTP, often decrypt TLS
ProtocolsAny TCP or UDP (databases, games, MQTT)HTTP, HTTPS, HTTP/2, gRPC, WebSocket
Client IP at the serverCan be kept as isUsually passed in X-Forwarded-For
Good forRaw speed, static IPs, non-HTTP trafficMicroservices, path routing, auth at the edge
In everyday words
An L4 balancer is a postman who sorts by the address on the envelope. An L7 balancer is a secretary who opens the letter and hands it to the right department.
Actually
AWS says a Network Load Balancer "functions at the fourth layer of the OSI model". For TCP it picks a target with a flow hash of protocol, source IP, source port, destination IP, destination port and TCP sequence number. Each TCP connection stays on one target for its whole life. ALB, the L7 one, can route by path and run auth before routing. AWS: What is a Network Load Balancer?

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

How the balancer picks the next server core

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.

AlgorithmRuleWorked exampleUse it whenWatch out
Round robinTake 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 robinTurns 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 connectionsPick 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 timePick 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 hashhash(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 hashingServers 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 choicesPick 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.
nginx.conf · open source NGINX
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;
}
In everyday words
Round robin is dealing cards one by one. Least connections is joining the shortest queue. Hashing is "people with surnames A to F go to desk 1".
Actually
NGINX docs: "By default, requests are distributed between the servers using a weighted round-robin balancing method." 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 module

Default 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

How the balancer finds out a server is sick core

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 settingDefaultMeaning
HealthCheckIntervalSeconds30 sTime between checks of one target (instance or IP targets).
HealthCheckTimeoutSeconds5 sNo answer in this time counts as a failure.
UnhealthyThresholdCount2Failures in a row before the target is taken out.
HealthyThresholdCount5Passes in a row before it is put back.
Matcher200HTTP 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.

In everyday words
A teacher calls the roll every 30 seconds. A pupil who misses two calls is marked absent. He must answer five calls in a row to be marked present again.
Actually
ALB "fails open": "If a target group contains only unhealthy registered targets, the load balancer routes requests to all those targets, regardless of their health status." The reason: a bad health check page should not take the whole site down. NGINX open source uses passive checks: max_fails defaults to 1 and fail_timeout to 10 s. As of both docs, checked 2026-10-25. AWS: ALB health checks
Shallow check
Returns 200 if the process is up. Cheap. Misses a broken database link.
Deep check
Also tests the database and caches. Danger: if the shared DB is slow, every server fails at once.
Slow start
ALB can ramp traffic to a new target linearly, so a cold server is not flooded.

Make 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

Two features the LB offers, one you should mostly avoid interview favourite

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.

Uneven load
Heavy users stay stuck to one server. New servers get only new users.
Lost state
If that server dies, its users lose their carts and sessions.
Slow scale-in
You cannot remove a server until its sticky users leave.

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.

ModeHowGoodBad
Terminate at LBLB holds the certificate and decrypts.One place for certificates; LB can read HTTP and route.Traffic inside is plain unless you re-encrypt.
Re-encryptLB decrypts, then opens a new TLS link to the server.L7 features and encryption all the way.Two TLS handshakes; servers need certificates too.
PassthroughL4 LB forwards encrypted bytes untouched.Only the server sees plain text.No path routing, no header-based features at the LB.
In everyday words
Sticky sessions are like always needing the same waiter, because only he remembers your order. Write the order on a ticket instead, and any waiter can serve you.
Actually
ALB duration-based stickiness uses a cookie named 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 listener

Terminate TLS at the LB, keep servers stateless, and treat sticky sessions as a last resort.

Source: AWS: ALB sticky sessions · AWS: ALB HTTPS listener

06Forward proxy, reverse proxy, load balancer, API gateway

Four middlemen that are easy to mix up interview favourite

Forward proxy versus reverse proxy Top row: a laptop sends requests through a forward proxy to any website, so the proxy works for the client. Bottom row: any user reaches an API gateway, which forwards to your private services, so the gateway works for the servers. FORWARD PROXY · WORKS FOR THE CLIENT REVERSE PROXY · WORKS FOR THE SERVERS Your laptopoffice network Forward proxyfilter, hide client Any websiteinternet Any userinternet API gatewayauth, rate limit Your servicesprivate LEGEND Caller Proxy Gateway Outside Your side
Same shape, opposite owner. The forward proxy belongs to the client; the reverse proxy belongs to the servers.
MiddlemanWho owns itMain jobExample
Forward proxyThe client sideFilter or hide outgoing traffic, cache downloadsOffice web proxy, Squid
Reverse proxyThe server sideStand in front of servers: TLS, caching, compressionNGINX, HAProxy
Load balancerThe server sideA reverse proxy whose main job is spreading loadAWS ALB, NLB
API gatewayThe server sideA reverse proxy for APIs: auth, rate limits, routing, aggregationAmazon API Gateway, Kong
Auth
Check the token once at the door, so services trust the gateway.
Rate limiting
Cap each API key before traffic reaches services.
Routing
/orders goes to the orders service, /users to users.
Aggregation
One client call fans out to 3 services; the gateway merges replies.
In everyday words
A forward proxy is your own assistant who makes calls for you. A reverse proxy is a company receptionist who takes calls for the staff.
Actually
RFC 9110 defines both. A proxy is "a message-forwarding agent that is chosen by the client". A gateway, "a.k.a. 'reverse proxy'", is "an intermediary that acts as an origin server for the outbound connection but translates received requests and forwards them inbound to another server or servers". Amazon API Gateway throttles "using the token bucket algorithm" and returns 429 Too Many Requests when limits are hit. RFC 9110 §3.7 Intermediaries

Every 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

Picking a region, and keeping the front door itself alive deep dive

Global routing to the nearest region A user in Paris and a user in Tokyo reach a global router using GeoDNS or anycast. It sends the Paris user to the Europe region and the Tokyo user to the Tokyo region. Paris userFrance Tokyo userJapan Global routerGeoDNS or anycast Europe regioneu-west-3 Tokyo regionap-northeast-1 LEGEND User Picks a region Region with its own LBs and servers
Global routing picks a region. Inside each region, a normal LB picks a server.
MethodHow it picksFailover speedExample
GeoDNSDNS 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
AnycastSame 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.

SetupHow it worksGoodBad
Active-passiveOne 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-activeSeveral 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 LBThe provider runs many nodes per zone and scales them for you.No SPOF to manage yourself.Less control; you trust the provider's limits.
In everyday words
GeoDNS is a phone book that gives a different number depending on your city. Anycast is one phone number that rings at the nearest office.
Actually
RFC 4786 defines anycast as making one service address "available in multiple, discrete, autonomous locations, such that datagrams sent are routed to one of several available locations". AWS Global Accelerator gives two static IPv4 addresses that "are anycast from the AWS edge network". Traffic enters AWS "from the edge location that is closest to your users". Route 53 lists failover routing as the way "to configure active-passive failover". AWS: How Global Accelerator works

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

Two bucket pictures that behave very differently with bursts interview favourite

Token bucket A refill adds 2 tokens per second into a bucket that holds at most 10 tokens. Each request takes one token. If a token is there, the request goes through; if the bucket is empty, the request is rejected with HTTP 429. TOKEN EMPTY Refill+2 tokens / s Requests1 token each Token bucketmax 10 tokens Servedgoes through RejectedHTTP 429 LEGEND Input Bucket Allowed Rejected
Capacity sets the biggest burst. Refill rate sets the long-run average.

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.

QuestionToken bucketLeaky bucket
BurstsAllowed, up to capacitySmoothed into a steady stream
Output rateCan spike brieflyAlways steady
State per keyToken count + last refill timeQueue (or a "water level" + time)
Extra delayNone: allow or reject nowRequests may wait in the queue
Used byAmazon API Gateway, StripeNGINX limit_req
nginx.conf · leaky bucket with a burst queue
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
}
In everyday words
Token bucket is a bus pass with saved rides: save up, then use many at once. Leaky bucket is a ticket gate that lets one person through every few seconds, whatever the crowd.
Actually
Amazon API Gateway: "throttling rate" is "the rate ... that tokens are added to the token bucket" and "throttling burst" is "the capacity of the token bucket". NGINX 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 throttling

Choose 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

Counting requests per minute, three ways deep dive

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.

Sliding window counter worked example The previous minute had 42 requests and the current minute has 18 so far, 15 seconds in. The sliding 60 second window covers the last 45 seconds of the previous minute and the first 15 seconds of the current minute. THE 60 S WINDOW WE COUNT Previous minute42 requests Current minute18 so far 0 s 60 s now = 75 s 120 s LEGEND Old window New window Last 60 s, estimated
The window covers 75% of the old minute and 15 s of the new one.
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.

AlgorithmMemory per keyAccuracyAllows bursts at edgesBest for
Fixed window1 counterLow at edgesYes, up to 2× the limitSimple quotas (per day)
Sliding window log1 entry per requestExactNoLow volume, strict limits
Sliding window counter2 countersClose (an estimate)Mostly NoHigh volume APIs
Token bucket2 numbersExact for its modelYes, by design (capacity)APIs with fair bursts
Leaky bucketQueue or 2 numbersExact for its modelNo, smooths themProtecting a steady backend
In everyday words
Fixed window is a gym that resets your entry count at midnight. Sliding window asks "how many times in the last 24 hours?", whatever the clock says.
Actually
Cloudflare's 2017 post uses exactly this formula: "rate = 42 * ((60-15)/60) + 18 = 42 * 0.75 + 18 = 49.5 requests" against a 50 per minute limit. Over 400 million requests, "0.003% of requests have been wrongly allowed or rate limited", with "an average difference of 6% between real rate and the approximate rate". They rejected fixed windows because the counter resets "allowing regular traffic spikes", and the log because it "has huge processing and memory requirements". Cloudflare: How we built rate limiting

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

Many gateway copies, one shared count deep dive

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.

Rate limit check with Redis A client calls the gateway. The gateway runs a Lua script on Redis that increments the counter. Redis returns count 101. The gateway answers 429 Too Many Requests. GET /ORDERS EVAL LIMIT.LUA COUNT = 101 429 TOO MANY ClientAPI key k1 Gateway copy1 of 10 Redisshared counter LEGEND Caller Limiter One shared count Reply
Every gateway copy asks the same Redis key, so the limit holds across the fleet.

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.

limit.lua · fixed window, atomic (from the Redis INCR docs pattern)
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
sliding window log with a sorted set (run inside one Lua script too)
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

PlaceGood forWeak spot
Edge / CDN / WAFFloods and bots by IP, before they cost you anythingDoes not know users or plans
API gatewayPer API key or per user limits, one place for all servicesNeeds a shared store for many copies
Inside each serviceCostly endpoints, per-tenant fairness, concurrency capsEvery team must build it
Client SDKBeing polite: back off before the server says noCannot be trusted; never the only guard
Fail open or fail closed?
If Redis is down, the limiter cannot count. Most APIs fail open (allow), because blocking all users is worse than a short gap in limits. Login and payment endpoints may fail closed. This is a design choice; there is no single standard. Stripe's 2017 post advises building limiters so they fail safely when the store has errors.
In everyday words
Ten doors into one stadium need one shared turnstile count, or each door lets in a full crowd.
Actually
RFC 6585 §4: "The 429 status code indicates that the user has sent too many requests in a given amount of time". It "MAY include a Retry-After header", and 429 responses "MUST NOT be stored by a cache". 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 §4

Shared 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 decision table

SituationPickWhy
Microservices behind one domain, routed by pathL7 LB or API gatewayOnly L7 can read /orders vs /users.
Game server, MQTT, database proxy, or need a static IPL4 LB (NLB)Non-HTTP traffic; per-connection routing; static IP per zone.
Same-size servers, short similar requestsRound robinSimplest; fair enough.
Long and short requests mixed (uploads, WebSocket)Least connections / least outstanding requestsCounts real work in progress, not turns.
Cache cluster or shards behind the LBConsistent hashingAdding a node moves only a small share of keys.
Server keeps session in memory and you cannot change it yetSticky sessions (for now)Works today; plan to move state to Redis or tokens.
Users on several continentsGeoDNS or anycast in front of regional LBsLower latency; region failover.
Public API with fair short burstsToken bucketAllows bursts up to capacity, steady average.
Backend that must see a steady paceLeaky bucket (queue)Smooths spikes.
High-volume per-key limits, many gateway copiesSliding window counter in Redis via LuaTwo numbers per key, atomic, shared.
Daily or monthly quotaFixed windowEdge bursts do not matter at that scale.

—Common mistakes and traps

Things that look right and break in production traps

Deep health checks
Health page calls the shared database
DB slows down, every server fails its check at once. Keep checks local.
Per-node counters
Rate limit kept in each server's memory
N copies means N times the limit. Use a shared store.
INCR then EXPIRE
Two separate calls
A crash between them leaves a key that never expires. Use one Lua script.
One LB box
The front door is a single point of failure
Run a pair (active-passive) or a fleet (active-active).
Limit by IP only
Offices and mobile carriers share IPs
Thousands of real users look like one attacker. Limit by API key or user when you can.
No Retry-After
Bare 429 with no hint
Clients retry at once and make the flood worse.
Low DNS TTL hopes
Expecting DNS failover in seconds
Resolvers and clients cache answers. Anycast fails over faster.
Sticky by default
Turning on stickiness to hide a state bug
Uneven load and lost sessions on failure. Make servers stateless.

—Interview questions

20 questions, easy to design-level. Try first, then open the answer drill

1. What does a load balancer do, in one minute? easy
Show answer
It gives clients one address and spreads their requests over many servers. It also runs health checks and stops sending traffic to broken servers. That gives us scale (add servers behind the same name), availability (failures are hidden) and safe deploys (drain one server at a time). Many LBs also terminate TLS and add headers like 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.
2. L4 vs L7 load balancing: what is the difference? easy
Show answer
L4 works on TCP/UDP. It sees IPs and ports and picks a server per connection, without reading the content. L7 reads the HTTP request, so it can route by path, host, header or cookie, and do auth or rewrites. L4 is faster per request and works for any protocol; L7 is smarter but must parse HTTP and usually decrypt TLS. AWS NLB is L4 and hashes the 5-tuple plus TCP sequence number; ALB is L7. I pick L7 for microservices on one domain and L4 for databases, games or when I need static IPs.
3. Name the common LB algorithms and when you use each. easy
Show answer
Round robin for equal servers and similar requests. Weighted round robin when servers differ in size. Least connections when request lengths vary, like uploads or WebSockets. Least response time when servers differ in speed at run time. IP hash for simple stickiness. Consistent hashing when keys must stay on the same node, like a cache cluster, because adding a node moves only about 1/N of keys. "Power of two choices" (pick 2 at random, take the less loaded) works well when many balancers have no shared view. NGINX default is weighted round robin; ALB default is round robin.
4. Why is least connections better than round robin for WebSockets? medium
Show answer
A WebSocket stays open for minutes or hours. Round robin counts turns, not load. After a deploy, the old servers keep thousands of long connections while round robin still gives them equal new ones. Least connections sends new sockets to the server with the fewest open, so load evens out. Trade-off: a brand-new server has zero connections and can get flooded at once, so pair it with slow start or a cap. With the least outstanding requests algorithm, AWS ALB picks the WebSocket target that way, then keeps all messages on that one connection.
5. How do health checks work and what can go wrong? medium
Show answer
The LB sends a test request, like 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.
6. What are sticky sessions and why avoid them? easy
Show answer
Sticky sessions pin a user to one server, usually with an LB cookie like ALB's 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.
7. Where should TLS be terminated? medium
Show answer
Usually at the L7 load balancer. It holds the certificate in one place, decrypts once, and can then read HTTP to route and add headers. Servers get plain HTTP in the private network. If rules require encryption everywhere, the LB re-encrypts to the servers (two handshakes, more CPU). If even the LB must not see plain text, use L4 passthrough, as AWS suggests with an NLB TCP listener on 443; you then lose path routing at the LB. Certificates usually come from a managed service like ACM, which also renews them.
8. Forward proxy vs reverse proxy vs load balancer vs API gateway? easy
Show answer
A forward proxy works for clients: it sends their requests out, often to filter or hide them. A reverse proxy works for servers: clients think it is the server, and it forwards inside. RFC 9110 calls it a gateway, "a.k.a. reverse proxy". A load balancer is a reverse proxy focused on spreading load. An API gateway is a reverse proxy focused on APIs: auth, rate limits, routing, request transforms and aggregation (one call fans out to many services). In practice one product, like NGINX or Envoy, can play all server-side roles.
9. The load balancer is a single point of failure. How do you fix that? medium
Show answer
Never run one LB box. Active-passive: two LBs share a virtual IP; the standby watches heartbeats and takes the IP when the active dies (VRRP, keepalived). Simple, but half sits idle and failover takes seconds. Active-active: several LBs serve at once behind DNS or anycast; losing one costs a slice of capacity, so keep spare room. Managed cloud LBs already run several nodes per zone. One layer up, put regions behind GeoDNS or anycast so a whole region can fail. Follow-up: shared state like rate-limit counters must then live outside the LB.
10. GeoDNS vs anycast for global traffic? medium
Show answer
GeoDNS returns different IPs based on where the resolver or user is, like Route 53 geolocation or latency routing. It is flexible and easy, but failover waits for DNS caches to expire, and the resolver's location may not match the user. Anycast announces one IP from many sites; internet routing (BGP) sends packets to a nearby site. Failover is fast because routing moves, not caches. AWS Global Accelerator gives two static anycast IPs and enters the AWS network at the nearest edge. Many systems use both: anycast for the edge, GeoDNS for region choice.
11. Explain the token bucket with numbers. easy
Show answer
A bucket holds tokens up to a capacity, say 10. Tokens refill at a rate, say 2 per second. Each request takes one token; with none left, reply 429. A quiet client can burst 10 requests at once, then gets 2 per second. State per key is just two numbers: tokens and last refill time; you add tokens lazily when a request comes. Amazon API Gateway uses this: "throttling rate" is the refill rate and "throttling burst" is the bucket capacity. Stripe also uses token buckets in Redis.
12. Token bucket vs leaky bucket? medium
Show answer
Token bucket allows bursts up to its capacity and rejects immediately when empty; output can spike. Leaky bucket queues requests and lets them out at a fixed pace; output is smooth and extra requests wait or overflow. Use token bucket for public APIs, where short bursts from real users are normal. Use leaky bucket in front of something fragile that needs a steady rate, like a legacy DB or a payment partner. NGINX limit_req is leaky bucket; with burst=5 nodelay it serves a small burst at once and rejects the rest.
13. What is wrong with a fixed window counter? medium
Show answer
The counter resets at the window edge. With a limit of 100 per minute, a client can send 100 at 10:05:59 and 100 at 10:06:00: 200 in two seconds, both allowed. So the true peak can be twice the limit. It is still fine for daily quotas or rough protection, because it is one 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.
14. Walk me through the sliding window counter math. medium
Show answer
Keep a counter for the previous minute and the current one. When a request comes at 15 s into the current minute, the last 60 s covers 45 s of the old minute. Estimate = previous × 45/60 + current. Cloudflare's example: 42 × 0.75 + 18 = 49.5, under a limit of 50, so allow. It assumes the old minute's requests were spread evenly. Cloudflare measured 0.003% wrong decisions over 400 million requests, with an average 6% gap from the real rate. Cost is two numbers per key, so it scales to millions of keys.
15. How do you rate limit across 20 gateway instances? hard
Show answer
Keep the counter in one shared store, usually Redis, keyed like 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.
16. What should the response look like when you rate limit? easy
Show answer
HTTP 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.
17. Where would you place rate limiting in a system? medium
Show answer
In layers. At the edge (CDN or WAF) for floods and bots by IP, before they cost anything. At the API gateway for per-key or per-user plans, using a shared Redis. Inside services for costly endpoints and concurrency caps, like Stripe's concurrent request limiter. Plus load shedding when the fleet is overloaded, which drops low-priority traffic first. Client SDKs can back off politely but are never trusted. Stripe's 2017 post lists four limiters: request rate, concurrent requests, fleet usage shedder and worker utilization shedder.
18. Design a rate limiter for a public API with free and paid plans. hard
Show answer
Requirements: per-key limits by plan (say free 10/s, paid 100/s), low latency, works across many gateways, clear 429s. Design: the gateway resolves the API key to a plan (cached config). It runs a token bucket or sliding window counter in Redis via one Lua script, keyed by API key. On deny, return 429 with Retry-After. Store plan limits in a config service so changes need no deploy. Scale Redis by sharding on key. Handle Redis failure by failing open with a local fallback limit. Add metrics on denials per key. Follow-ups: hot keys, multi-region (accept a per-region budget).
19. What breaks if the rate limiter's Redis goes down? hard
Show answer
The gateways cannot count. If they fail closed, every request is rejected and the whole API is down, so the limiter caused the outage. If they fail open, limits vanish and an abuser can flood the backend. Most teams fail open with a local in-memory fallback limit per instance, which is rough but bounded (N instances × local limit). Use short timeouts on the Redis call so a slow Redis does not slow every request. Run Redis with a replica and automatic failover. Alert on limiter errors. For login or payment endpoints, failing closed may be correct.
20. How would you scale a load balancing layer to millions of requests per second worldwide? hard
Show answer
Layer it. First, anycast or GeoDNS sends each user to a nearby region or edge site. In each region, an L4 tier (like NLB) spreads connections across many L7 proxies; L4 is cheap per packet and scales wide. The L7 tier (ALB, Envoy or NGINX fleet) terminates TLS and routes by path to service pools, using least-requests or power-of-two choices. Keep the LBs stateless; keep sessions and limits in shared stores. Use health checks, slow start and connection draining for safe deploys. AWS says an NLB "can handle millions of requests per second".

—Cheat sheet

Numbers and facts to remember memorise

FactValueSource
ALB health check interval / timeout30 s / 5 s (instance, IP targets)AWS ALB health checks
ALB unhealthy / healthy threshold2 fails / 5 passesAWS ALB health checks
ALB when all targets unhealthyFails open: routes to allAWS ALB health checks
ALB deregistration delay300 s defaultAWS ALB target group attributes
ALB algorithmsRound robin (default), least outstanding requests, weighted randomAWS ALB target group attributes
ALB sticky cookieAWSALB, cookie expiry 7 daysAWS ALB sticky sessions
NLB TCP hash inputsProtocol, src IP, src port, dst IP, dst port, TCP sequenceAWS NLB intro
NGINX default algorithmWeighted round robinNGINX upstream
NGINX max_fails / fail_timeout1 / 10 sNGINX upstream
NGINX limit_reqLeaky bucket; rejects with 503 unless changedNGINX limit_req
API Gateway throttlingToken bucket: rate = refill, burst = capacityAWS API Gateway
Global Accelerator2 static anycast IPv4 addressesAWS Global Accelerator
429 Too Many RequestsRFC 6585 §4; must not be cachedRFC 6585
Retry-AfterSeconds or HTTP dateRFC 9110 §10.2.3
Sliding window counterprev × (60 − t)/60 + currentCloudflare 2017
Its accuracy0.003% wrong of 400 millionCloudflare 2017
Fixed window worst caseAbout 2× the limit at an edgeDerived from the algorithm

—Sources

All checked 2026-10-25 references

WhatWhere
ALB health check settings and fail-opendocs.aws.amazon.com: ALB target group health checks
ALB algorithms, stickiness, slow start, deregistration delaydocs.aws.amazon.com: ALB target group attributes
ALB TLS terminationdocs.aws.amazon.com: ALB HTTPS listener
NLB layer 4 and flow hashdocs.aws.amazon.com: What is a Network Load Balancer?
NGINX algorithms, passive checks, stickynginx.org: ngx_http_upstream_module
NGINX leaky bucket limiternginx.org: ngx_http_limit_req_module
HAProxy balance algorithmsdocs.haproxy.org: configuration, balance
Consistent hashing originKarger et al., STOC 1997
Proxy vs gateway (reverse proxy)RFC 9110 §3.7
Retry-AfterRFC 9110 §10.2.3
429 Too Many RequestsRFC 6585 §4
Anycast definitionRFC 4786
VRRP (active-passive virtual IP)RFC 5798
Anycast static IPs at the AWS edgedocs.aws.amazon.com: How Global Accelerator works
GeoDNS and failover routingdocs.aws.amazon.com: Route 53 routing policies
API Gateway token bucket and 429docs.aws.amazon.com: API Gateway throttling
Gateway aggregation patternlearn.microsoft.com: Gateway Aggregation
Stripe's four limiters, token bucket in Redisstripe.com/blog/rate-limiters (2017)
Sliding window counter formula and accuracyblog.cloudflare.com (2017)
Redis INCR rate limiter pattern and raceredis.io: INCR