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:

ScenarioRatioExample
Identical servers1:1:1weight=1 for all
Server 2x stronger2:1weight=2, weight=1
Mixed hardware generations4:2:1weight=4, weight=2, weight=1
5% canary19:1weight=95, weight=5
10% canary9:1weight=9, weight=1
20% canary4:1weight=4, weight=1
50% canary1:1weight=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=N controls traffic proportions relatively — a server with weight=3 gets 3x more requests than a server with weight=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=5 out of a total weight=100), monitor the error rate, increase gradually.
  • Blue-green deployment: use weight=0 backup for the standby version, switch by changing the configuration and running nginx -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.

← Previous: IP Hash   Next: Health Check →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact