Skip to content
Slow Start: Ramping New Backends
step 1/5

Reading — step 1 of 5

Read

~3 min readProduction Concerns

Slow Start: Ramping New Backends

A backend that just booted is functionally slower than its steady-state self: application caches are empty, database connection pools are unopened, JIT compilers haven't warmed, CPU caches are cold. If the balancer hands it a full share of traffic in its first second, those first requests run slow — and now your own protective machinery betrays you. Timeouts fire, passive health checks count failures, circuit breakers trip, and the brand-new backend gets ejected... then re-added, cold again, and ejected again. This oscillation is flapping, and it can keep a perfectly good server out of rotation indefinitely.

Slow start fixes it by ramping a new backend's effective weight linearly from 0 to its configured target over a window:

effective_weight(t) = target_weight * min(1, (t - joined_at) / ramp_secs)

A backend with target weight 10 and a 10-second ramp:

t=0   weight 0     (no traffic yet)
t=5   weight 5     (half share)
t=10  weight 10    (fully ramped)

Every serious balancer ships this: NGINX Plus (slow_start=30s on the upstream server), HAProxy (slowstart 30s), Envoy (slow_start_config, with a configurable aggression curve), and AWS ALB (slow start on target groups, configurable 30–900 seconds, off by default). And note the second trigger: slow start applies not just to new backends but to backends recovering from DOWN — a server that just passed its health checks has the same cold-cache problem as a fresh one, and blasting it on recovery is how you re-kill it.

Plugging into smooth WRR

The elegance of the effective-weight formulation is that the selection algorithm doesn't change. You still run smooth weighted round-robin (lesson 2) — you just recompute each backend's effective weight at pick time and use that instead of the static target:

  1. For each backend with effective weight > 0: add its effective weight to its current.
  2. Pick the highest current (ties → lexicographically smaller name).
  3. Subtract the total of the participating effective weights from the winner.

Two integration details matter. A backend whose effective weight is 0 is skipped entirely — it neither accumulates current nor competes; a just-added backend must receive no traffic at t=0, not "a little". And the current counters persist across picks as always; only the weights fed into each round change as time advances.

Your exercise

Implement eff_weight() and pick() in the starter's SlowStartPool. Commands: POOL <name>:<w> ... (all at full target, no ramp), NOW <t> (integer sim time), ADD <name>:<w> <ramp_secs> (joins at the current time, ramping), WEIGHTS (print <name>:<effective_weight> sorted by name), PICK (smooth WRR over current effective weights, or NONE if every weight is 0).

Grader traps:

  • Truncate the effective weight, never round. ADD b:10 20 then NOW 5: 10 × 5/20 = 2.5 → b:2. Rounding to 3 fails the hidden test. (Integer math target * elapsed // ramp gets it right.)
  • Zero means invisible. POOL a:5, ADD b:5 5, then five PICKs at t=0 must print a five times — b is at weight 0 and completely absent from the rotation.
  • Fully-ramped is capped. ADD b:5 10 then NOW 100b:5, never more; min(1, elapsed/ramp) caps the factor.
  • Smooth-WRR ties are lexicographic here. POOL a:6 b:2: picks go a a b a — on the second pick both currents hit 4 and a wins the tie. If your tie falls to b you'll see a b ... and fail.

Discussion

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

Sign in to post a comment or reply.

Loading…