Weighted Load Balancing #
Weight lets you control how much traffic each server receives proportionally. By default all servers have weight 1 — even distribution. With weights, you can reflect hardware capacity differences, do gradual canary deployments, or run zero-downtime blue-green deployment strategies.
How Weight Works #
Weight is a relative number. What matters isn’t the absolute value, but the ratio between servers:
upstream app_servers {
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000 weight=1;
}
Out of every 4 requests:
- 3 requests → Server 10.0.0.1 (weight 3)
- 1 request → Server 10.0.0.2 (weight 1)
weight=3 and weight=1 produce a distribution identical to weight=6 and weight=2, or weight=30 and weight=10. Nginx distributes based on proportion.
Without weight (or with weight=1), all servers get the same distribution:
weight=1, weight=1, weight=1 → 33.3%, 33.3%, 33.3%
weight=2, weight=1, weight=1 → 50%, 25%, 25%
weight=3, weight=2, weight=1 → 50%, 33.3%, 16.7%
weight=5, weight=3, weight=2 → 50%, 30%, 20%
Use Case 1: Servers with Different Hardware Capacity #
The most common real-world scenario: infrastructure made up of servers with non-uniform specs — due to gradual upgrades, legacy from an earlier era, or spot instance usage:
upstream production_cluster {
# Latest-generation server (high-end)
# 32 cores, 128GB RAM, NVMe SSD
server 10.0.0.1:3000 weight=8;
# Mid-generation server
# 16 cores, 64GB RAM, SSD
server 10.0.0.2:3000 weight=4;
# Older server still in use
# 8 cores, 32GB RAM, HDD
server 10.0.0.3:3000 weight=2;
# Minimal server (a backup that also serves normal traffic)
# 4 cores, 16GB RAM
server 10.0.0.4:3000 weight=1;
# Total: 15 parts
# Server 1: 8/15 = 53% of traffic
# Server 2: 4/15 = 27% of traffic
# Server 3: 2/15 = 13% of traffic
# Server 4: 1/15 = 7% of traffic
keepalive 32;
zone production_upstream 128k;
}
Use Case 2: Canary Deployment #
Canary deployment is a technique for releasing a new version by first sending a small portion of traffic for validation before the full rollout. Weight is the easiest way to implement this in Nginx:
flowchart LR
subgraph PHASE1["Phase 1: Canary 5%"]
P1A["v1 weight=95\n(servers 1, 2, 3)"]
P1B["v2 weight=5\n(server 4) canary"]
end
subgraph PHASE2["Phase 2: Progress to 20%"]
P2A["v1 weight=80\n(servers 1, 2)"]
P2B["v2 weight=20\n(servers 3, 4)"]
end
subgraph PHASE3["Phase 3: Full Rollout"]
P3A["v2 weight=1\n(all servers)"]
end
PHASE1 --> PHASE2
PHASE2 --> PHASE3# Phase 1: Start with 5% to the new version
upstream app_servers {
server 10.0.0.1:3000 weight=95; # v1 — production
server 10.0.0.2:3000 weight=95; # v1 — production
server 10.0.0.3:3000 weight=10; # v2 — canary (5% of the total)
}
# How to change weights without downtime:
# 1. Edit the configuration
vim /etc/nginx/conf.d/myapp.conf
# Phase 2: Raise to 20%
# server 10.0.0.1:3000 weight=80; # v1
# server 10.0.0.2:3000 weight=20; # v2
# ...
# 2. Test the configuration
nginx -t
# 3. Reload without downtime
nginx -s reload
Monitoring the Canary with Logging #
http {
# Map the version based on the selected upstream server
map $upstream_addr $app_version {
"~10\.0\.0\.[12]:" "v1";
"~10\.0\.0\.3:" "v2-canary";
default "unknown";
}
log_format canary_log '$remote_addr [$time_local] "$request" '
'$status $app_version $upstream_response_time';
server {
access_log /var/log/nginx/canary.log canary_log;
}
}
# Analyze the error rate per version
awk '{print $6, $5}' /var/log/nginx/canary.log | \
awk '{total[$1]++; if($2>=500) err[$1]++} END {for(k in total) print k, err[k]+0, total[k], (err[k]+0)/total[k]*100"%"}' | \
sort
# Output:
# v1 12 9823 0.12%
# v2-canary 8 489 1.63% ← high error rate, rollback!
Use Case 3: Zero-Downtime Blue-Green Deployment #
Blue-green deployment runs two production environments (blue = old version, green = new version) and switches traffic all at once:
upstream app_servers {
# BLUE: the currently active version
server 10.0.0.1:3000 weight=1; # blue-1
server 10.0.0.2:3000 weight=1; # blue-2
# GREEN: new version — ready but not yet receiving traffic
server 10.0.0.3:3000 weight=0 backup; # green-1
server 10.0.0.4:3000 weight=0 backup; # green-2
}
At the switch: edit the configuration, enable green, disable blue:
upstream app_servers {
# BLUE: disabled
server 10.0.0.1:3000 down;
server 10.0.0.2:3000 down;
# GREEN: now receiving all traffic
server 10.0.0.3:3000 weight=1;
server 10.0.0.4:3000 weight=1;
}
If there’s a problem, roll back instantly by reversing the configuration again and running nginx -s reload.
Combining Weight with Other Algorithms #
Weighted Least Connections #
upstream app_servers {
least_conn;
# A stronger server needs more connections before being considered "busy"
server 10.0.0.1:3000 weight=4; # 16 cores
server 10.0.0.2:3000 weight=2; # 8 cores
server 10.0.0.3:3000 weight=1; # 4 cores
keepalive 32;
zone app_upstream 64k;
}
Selection formula: active_connections / weight — the server with the smallest value is chosen. A larger-capacity server “needs” more connections before being considered busier than a smaller server.
Weighted IP Hash #
upstream app_servers {
ip_hash;
# Session persistence + weight
server 10.0.0.1:3000 weight=3;
server 10.0.0.2:3000 weight=1;
}
With ip_hash + weight, users are still guaranteed to the same server, but the user distribution across servers accounts for weights. Higher-weight servers get more “slots” in the hash ring.
Applying Weights for Auto-Scaling #
In cloud environments with auto-scaling, weights can be managed programmatically:
# Script: add a new server with a low weight first (ramp-up)
add_server_with_ramp_up() {
local new_ip=$1
local config="/etc/nginx/conf.d/upstream.conf"
# Add with a low weight (be careful, monitor first)
echo " server $new_ip:3000 weight=1;" >> $config
nginx -s reload
echo "Server $new_ip added with weight=1 (5% traffic)"
sleep 300 # Monitor for 5 minutes
# If no alerts, raise the weight
sed -i "s/$new_ip:3000 weight=1/$new_ip:3000 weight=3/" $config
nginx -s reload
echo "Weight raised to 3 (20% traffic)"
}
# Script: remove a server with a graceful drain
remove_server_gracefully() {
local ip=$1
local config="/etc/nginx/conf.d/upstream.conf"
# Lower the weight to 0 first (drain traffic)
sed -i "s/$ip:3000 weight=[0-9]*/$ip:3000 weight=1/" $config
nginx -s reload
echo "Weight lowered to 1"
sleep 60 # Wait for in-flight requests to finish
# Mark as down
sed -i "s/$ip:3000 weight=1/$ip:3000 down/" $config
nginx -s reload
echo "Server $ip marked down, safe to terminate"
}
Weight Recommendation Table #
Some commonly used weight ratios:
| Scenario | Ratio | Example |
|---|---|---|
| Identical servers | 1:1:1 | weight=1 for all |
| Server 2x stronger | 2:1 | weight=2, weight=1 |
| Mixed hardware generations | 4:2:1 | weight=4, weight=2, weight=1 |
| 5% canary | 19:1 | weight=95, weight=5 |
| 10% canary | 9:1 | weight=9, weight=1 |
| 20% canary | 4:1 | weight=4, weight=1 |
| 50% canary | 1:1 | weight=1, weight=1 |
Automating Weight Management in CI/CD #
For teams deploying frequently, weight management can be automated as part of the CI/CD pipeline:
#!/bin/bash
# deploy-canary.sh: Deploy a new version as a canary
NEW_SERVER=$1 # New server IP with the new version
INITIAL_WEIGHT=5 # Start with 5%
CONFIG="/etc/nginx/conf.d/upstream.conf"
# Step 1: Add the new server with a low weight
cat >> $CONFIG << EOF
server $NEW_SERVER:3000 weight=$INITIAL_WEIGHT; # canary v2
EOF
nginx -t && nginx -s reload
echo "Canary deployed: $NEW_SERVER with weight=$INITIAL_WEIGHT"
# Step 2: Monitor for 10 minutes
echo "Monitoring the error rate for 10 minutes..."
sleep 600
# Calculate the canary error rate from the log
ERROR_RATE=$(awk -v server="$NEW_SERVER" '
$5 ~ server && $6 >= 500 { err++ }
$5 ~ server { total++ }
END { if(total > 0) print int(err*100/total); else print 0 }
' /var/log/nginx/access.log)
echo "Canary error rate: ${ERROR_RATE}%"
if [ "$ERROR_RATE" -gt 5 ]; then
echo "HIGH ERROR RATE! Rolling back the canary..."
# Mark the server as down
sed -i "s/$NEW_SERVER:3000 weight=$INITIAL_WEIGHT/$NEW_SERVER:3000 down/" $CONFIG
nginx -t && nginx -s reload
echo "Rollback complete. Alert sent to the team."
exit 1
else
echo "Error rate OK. Ready for full rollout."
# Raise the weight to 50%
sed -i "s/$NEW_SERVER:3000 weight=$INITIAL_WEIGHT/$NEW_SERVER:3000 weight=50/" $CONFIG
nginx -t && nginx -s reload
fi
# GitHub Actions: deploy the canary as a step in the pipeline
jobs:
deploy-canary:
runs-on: ubuntu-latest
steps:
- name: Deploy canary server
run: |
ssh deploy@nginx-server \
'bash /usr/local/bin/deploy-canary.sh ${{ env.CANARY_IP }}'
- name: Monitor canary (10 minutes)
run: sleep 600
- name: Promote to 100% if healthy
run: |
ssh deploy@nginx-server \
'sed -i "s/weight=50/weight=100/g" /etc/nginx/conf.d/upstream.conf && nginx -s reload'
Weight Considerations for Different Workloads #
# I/O-bound workload (APIs, regular web servers)
# Distribution based on RAM and CPU is more relevant
upstream io_bound {
server web-1:3000 weight=4; # 16 cores, 32GB RAM
server web-2:3000 weight=2; # 8 cores, 16GB RAM
server web-3:3000 weight=1; # 4 cores, 8GB RAM
keepalive 32;
}
# CPU-bound workload (image processing, ML inference, video transcoding)
# CPU core count is more relevant than RAM
upstream cpu_bound {
server gpu-1:8000 weight=8; # 8 A100 GPUs
server gpu-2:8000 weight=4; # 4 A100 GPUs
server cpu-1:8001 weight=1; # CPU only, fallback
keepalive 8;
}
# Microservices with different SLAs
# Servers in the nearest region get higher weights
upstream geo_aware {
# Southeast Asia servers (close to Indonesian users)
server sg-1:3000 weight=10; # Singapore
server sg-2:3000 weight=10; # Singapore
# East Asia servers (higher latency)
server jp-1:3000 weight=3; # Japan
keepalive 16;
}
Verifying Weight Distribution #
After configuring weights, it’s important to verify that the traffic distribution matches expectations:
# Check the traffic distribution from the access log
# The log format must include $upstream_addr
awk '{print $5}' /var/log/nginx/access.log | \
sort | uniq -c | sort -rn
# Example output (weight=4, weight=2, weight=1):
# 4021 10.0.0.1:3000 ← ~57% (expected: 4/7 = 57.1%)
# 2009 10.0.0.2:3000 ← ~29% (expected: 2/7 = 28.6%)
# 978 10.0.0.3:3000 ← ~14% (expected: 1/7 = 14.3%)
# Total: 7008 requests → very accurate distribution!
# If the distribution doesn't match, likely:
# 1. A server is marked max_fails (temporarily out of rotation)
# 2. max_conns was reached on one server
# 3. No zone directive is used (data not synchronized between workers)
# Verify the active weights
nginx -T | grep -A 10 "upstream app_servers"
# Or show the running configuration:
nginx -T 2>/dev/null | grep -E "server.*weight"
Formula for Calculating Traffic Percentages #
Given: server A weight=5, server B weight=3, server C weight=2
Total weight = 5 + 3 + 2 = 10
Server A percentage = 5/10 × 100 = 50%
Server B percentage = 3/10 × 100 = 30%
Server C percentage = 2/10 × 100 = 20%
If traffic = 10,000 req/min:
Server A gets: 5,000 req/min
Server B gets: 3,000 req/min
Server C gets: 2,000 req/min
Weight Implications for High Availability #
One thing often overlooked: weights affect failover behavior. If a high-weight server goes down, the impact on overall capacity is larger:
Setup: Server A weight=8, Server B weight=2
Normal capacity: A handles 80%, B handles 20%
If Server A goes down:
All traffic goes to Server B
Server B, which usually handles only 20%, now must handle 100%
Server B most likely can't cope → overload!
A safer setup for HA:
Server A weight=3, Server B weight=2, Server C weight=1
Total 6 parts
If A is down: B gets 2/3 = 67%, C gets 1/3 = 33%
→ Still manageable if B and C are strong enough
# A configuration that accounts for failover capacity:
upstream production {
# Main servers: can each handle 100% of traffic in an emergency
server 10.0.0.1:3000 weight=3 max_fails=3 fail_timeout=30s;
server 10.0.0.2:3000 weight=3 max_fails=3 fail_timeout=30s;
# Helper server: smaller, but can cover if one main server goes down
server 10.0.0.3:3000 weight=1 max_fails=3 fail_timeout=30s;
# Backup: only if all the servers above are down
server 10.0.0.4:8080 backup;
keepalive 32;
zone prod_upstream 128k;
}
# Normal: A=3/7=43%, B=3/7=43%, C=1/7=14%
# If A is down: B=3/4=75%, C=1/4=25% (still reasonable)
# If A and B are down: C=100% (overloaded, but the backup server kicks in)
Checklist Before Using Weights in Production #
Before applying weight configuration in production, make sure all these points are met:
☐ Weights are calculated based on actual server capacity
(CPU cores, RAM, bandwidth, load-test benchmark results)
☐ The highest-weight server can handle at least 60%
of traffic on its own (anticipating another server going down)
☐ There's at least one backup server or a graceful degradation configuration
☐ The log format already includes $upstream_addr to verify distribution
☐ Active monitoring exists that detects if one server receives
far more traffic than expected
☐ If this is a canary deployment: there's already a rollback script or procedure
executable in < 2 minutes
☐ The configuration has been tested in staging with a load test
reflecting production traffic
☐ The team knows how to change weights and reload Nginx without downtime
Summary #
weight=Ncontrols traffic proportions relatively — a server withweight=3gets 3x more requests than a server withweight=1.- Useful for servers with different capacities so load distribution matches each one’s capability.
- Canary deployment: start with a small weight for the new version (e.g.,
weight=5out of a totalweight=100), monitor the error rate, increase gradually.- Blue-green deployment: use
weight=0 backupfor the standby version, switch by changing the configuration and runningnginx -s reload.- Can be combined with
least_conn— the effective formula =active_connections / weight; larger-capacity servers need more connections before being considered busy.- Weight changes can be done without downtime via
nginx -s reload— in-flight connections aren’t cut.