Skip to content
Round-Robin Load Balancing
step 1/5

Reading — step 1 of 5

Read

~3 min readBackend Pool & Selection

Round-Robin Load Balancing

A load balancer is the front door to a fleet: one address on the outside, N interchangeable backends on the inside. Every arriving request forces the same small decision — which backend gets this one? That selection function is the heart of the balancer; everything else in this course (health checks, stickiness, draining) is machinery wrapped around it.

Round-robin is the baseline answer: hand out backends in a fixed cycle.

backends: [server1, server2, server3]
request 1 -> server1
request 2 -> server2
request 3 -> server3
request 4 -> server1      # wrap around

The whole implementation is a counter and a modulo:

python

It is the default for a reason: nginx's upstream block, HAProxy's balance roundrobin, and AWS ALB all round-robin unless you configure otherwise. When requests cost roughly the same, an even count is an even load — and the decision needs no feedback from the backends at all: O(1), one integer of state.

Where it breaks

Round-robin is blind by design, and each blindness becomes a later lesson:

  • It ignores load. A 2-second /report counts the same as a 2 ms /ping. A struggling backend keeps receiving its full share while it drowns. Least-connections fixes this.
  • It ignores health. A dead backend stays in the cycle and eats every Nth request. Health checks fix this.
  • It ignores identity. The same client hops backends on every request, so server-local session state is lost. Sticky sessions fix this.

The traps

The empty pool. idx % len(backends) raises ZeroDivisionError when the pool is empty. A real balancer has to decide what "a request with zero backends" means (usually a 503); your implementation must handle it explicitly instead of crashing.

Stale index on pool change. If the pool shrinks from 5 backends to 3 while idx is 4, naive backends[idx] is an IndexError. Real balancers rebuild scheduler state on config reload; in this exercise, POOL resets the counter to 0 — the simplest correct policy.

Concurrency. Two threads that both read idx before either increments it pick the same backend. Production round-robin uses an atomic fetch-and-add (atomic.AddUint64 in Go, AtomicInteger.getAndIncrement in Java) — the counter is the whole critical section. Our grader is single-threaded, so a plain integer is fine here, but know why the real ones look different.

Your exercise

Implement the RoundRobin class behind the command loop in the starter: POOL <b1> <b2> ... replaces the backend list and resets the counter (prints OK), PICK prints the next backend, RESET zeroes the counter without touching the pool (prints OK), and PICK on an empty pool prints EMPTY.

Mistakes the grader will catch, straight from the tests:

  • Advancing before returning. The first PICK after POOL s1 s2 s3 must print s1, not s2.
  • Not resetting the cycle on POOL. After POOL alpha beta and three PICKs (alpha, beta, alpha), a new POOL gamma delta epsilon must make the next PICK print gamma — not continue at the old offset.
  • Mishandling repeated RESET. Every RESET prints OK, even back-to-back: on pool a b, the sequence PICK PICK RESET PICK RESET RESET PICK PICK must print exactly a b OK a OK OK a b.

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…