Skip to content

Weighted algorithm

The least connections article closed on a gap neither algorithm covered so far actually addresses: a backend pool is rarely made of identical machines. A newly added server with twice the CPU and memory of its older neighbors can handle proportionally more traffic without breaking a sweat, but round robin and plain least connections both treat every backend as interchangeable — one gets exactly the same rotation slot, or is judged by exactly the same connection count, as every other. Weighting is not a fifth algorithm sitting alongside the first two; it's a multiplier applied on top of them, telling the load balancer "this backend deserves a bigger share" without changing the underlying selection logic at all.

Weight as a multiplier, not a replacement

NGINX's server directive takes a weight parameter, defaulting to 1 for every backend if left unset — which is exactly why plain round robin and plain least connections, both covered in the previous two articles, are really just their weighted counterparts with every weight tied at the default value. Set the weights unevenly, and the same underlying algorithm starts favoring the heavier backend proportionally.

upstream backend {
    server backend1.example.com weight=5;
    server 127.0.0.1:8080;
    server unix:/tmp/backend3;
}

NGINX's own documentation for this exact configuration spells out the resulting distribution concretely: "each 7 requests will be distributed as follows: 5 requests go to backend1.example.com and one request to each of the second and third servers." Seven requests, one first backend at weight 5 and two others left at the implicit default of 1, and the split comes out 5:1:1 — not by coincidence, but because weight is literally the ratio the rotation follows.

Weights:  backend1=5   backend2=1   backend3=1
Requests: 1  2  3  4  5  6  7
Routed:   b1 b1 b1 b1 b1 b2 b3

Weighted round robin versus weighted least connections

Weighting layers onto either algorithm from the previous two articles, and it changes a different part of each one's logic.

Weighted round robin changes how often a backend's turn comes up in the rotation — a weight of 5 means that backend's slot appears five times as often as a backend at weight 1, exactly as the NGINX example above shows. It's still blind to what each backend is doing at the moment a request arrives; it just rotates through a differently proportioned list.

Weighted least connections changes what "least loaded" means, rather than how often a turn comes up. Instead of comparing raw connection counts directly, the load balancer effectively divides each backend's current connection count by its weight before comparing — a backend at weight 2 carrying 10 active connections is treated as equivalent in load to a backend at weight 1 carrying 5, since the heavier backend is expected to comfortably carry twice the concurrent work. NGINX's documentation for least_conn confirms this directly: the method picks the server "with the least number of active connections, taking into account weights of servers" — weight isn't a separate step bolted on afterward, it's baked into the same comparison the algorithm already makes.

Weight tells the algorithm how much a backend can carry; the algorithm still decides who gets the next request based on that adjusted capacity. Getting the ratio wrong doesn't break anything visibly at first — it just means the load balancer confidently sends traffic in proportions that don't match what the hardware underneath can actually sustain.

Where the weight number should come from

A weight is only as good as the estimate behind it, and the estimate is easy to get backwards. Weighting by a rough guess at "how much bigger" a new server feels, rather than measuring actual sustained capacity under realistic load, is the most common way this goes wrong: a server with double the CPU cores doesn't automatically handle double the request volume if the workload is memory-bound, disk-bound, or bottlenecked on a downstream dependency shared by every backend equally. The safer starting point is benchmarking each backend's actual sustained throughput independently — how many requests per second it holds steady at without its own latency degrading — and deriving the weight ratio from those numbers rather than from the spec sheet.

Practical scenario: a new server that barely gets used

A team adds a fourth backend — genuinely twice the CPU and RAM of the other three — to an existing pool, sets weight=2 while leaving the other three at the default, and expects it to absorb a meaningfully larger share of traffic. A week later, the new server's CPU utilization sits noticeably lower than its three older, less powerful siblings.

upstream api_pool {
    server 10.0.3.10:8080;
    server 10.0.3.11:8080;
    server 10.0.3.12:8080;
    server 10.0.3.13:8080 weight=2;
}

Checking the actual request distribution across a fixed window, rather than assuming the configuration is being ignored, is the right first step:

awk '{print $1}' /var/log/nginx/upstream_access.log | sort | uniq -c
1000 10.0.3.10
1000 10.0.3.11
1000 10.0.3.12
2000 10.0.3.13

The distribution is exactly right by the configuration's own math — three backends at the default weight of 1 and one at weight 2 means a 1:1:1:2 ratio, and 1000:1000:1000:2000 is precisely that ratio holding across 5000 total requests. The weight did what it was configured to do. The mismatch is between the ratio chosen and the hardware's actual relative capacity: weight=2 asked the new server to handle exactly double the share of one of the older machines, but the new server is capable of considerably more than double — the low CPU utilization is the new server comfortably absorbing its assigned share with room to spare, not evidence the configuration failed. The fix is raising the weight further, ideally based on a real throughput benchmark of the new hardware rather than another guess, and re-checking utilization afterward rather than assuming the first number chosen was correct.

Changing production weight values shifts real traffic immediately

Raising a backend's weight takes effect on the next configuration reload and immediately changes how much live traffic that server receives — there's no gradual ramp-up unless the load balancer or deployment tooling specifically provides one. Confirm the backend can actually absorb the increased share (via the kind of benchmark described above, run in a staging environment first) before applying a large weight change to a production pool, and watch the same per-backend metrics afterward to confirm the new ratio behaves as expected.

Practice exercises

  1. A pool has three backends at weights 3, 1, and 1 respectively, running weighted round robin. Out of the next 25 requests, how many should each backend receive, and why doesn't the answer come out to a perfectly even split?
  2. Explain, using the definition of weighted least connections above, why a backend at weight 4 holding 20 active connections might still receive the next request over a backend at weight 1 holding 3 active connections.
  3. A team sets a new backend's weight based on its listed CPU core count relative to the rest of the pool, without benchmarking it under real traffic. Using the practical scenario above, explain what could go wrong with that approach specifically.

Round robin, least connections, and weighting all share one trait: none of them look at who is making the request, only at the pool's current state or fixed capacity ratios. That's fine for a stateless service where any backend can serve any request equally well — it stops being fine the instant a backend needs to remember something about a specific client between requests. The next article covers the algorithm built for exactly that case.

Sources