How to Configure Nginx Load Balancing on RHEL 7
As web applications grow in traffic, a single backend server eventually becomes a bottleneck. Nginx’s upstream module provides built-in load balancing that distributes incoming requests across multiple backend servers, improving throughput, redundancy, and fault tolerance. Unlike Nginx Plus, the open-source version included in RHEL 7’s EPEL repository supports several powerful load-balancing algorithms — round-robin, least connections, and IP hash — along with configurable health detection through passive failure tracking. This tutorial covers every aspect of configuring Nginx load balancing on RHEL 7: the upstream block, all available algorithms, weight and connection limits, passive health checking with max_fails and fail_timeout, and practical techniques for verifying load distribution.
Prerequisites
- RHEL 7 with Nginx installed from EPEL
- Two or more backend application servers (or processes) to balance across
- Root or sudo access
- Nginx running and able to reach backend servers over the network
- SELinux httpd_can_network_connect boolean enabled (if backends are on different ports)
Step 1: Install Nginx and Start the Service
If Nginx is not already installed:
sudo yum install -y epel-release
sudo yum install -y nginx
sudo systemctl enable nginx
sudo systemctl start nginx
Open firewall ports:
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
Step 2: Define the Upstream Block
The upstream block in Nginx defines a named pool of backend servers. It must be placed inside the http block of /etc/nginx/nginx.conf, or in a separate file that is included by the http block. Create a configuration file for your load-balanced application:
sudo nano /etc/nginx/conf.d/myapp-lb.conf
Start with a basic round-robin upstream block with three backend servers:
upstream myapp_pool {
server 192.168.1.101:8080;
server 192.168.1.102:8080;
server 192.168.1.103:8080;
}
server {
listen 80;
server_name yourdomain.com;
location / {
proxy_pass http://myapp_pool;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Connection "";
}
}
The name myapp_pool is arbitrary — use any name that identifies the backend pool. The proxy_pass directive references it by that name.
Step 3: Round-Robin Load Balancing (Default)
Round-robin is Nginx’s default algorithm when no other directive is specified in the upstream block. Requests are distributed sequentially: request 1 goes to server 1, request 2 to server 2, request 3 to server 3, request 4 back to server 1, and so on.
Round-robin is suitable when all backends have equal capacity and session state is either stored centrally (e.g., Redis or a database) or is not required. No additional configuration is needed — the basic upstream block in Step 2 already uses round-robin.
Step 4: Weighted Round-Robin
When backends have different hardware capacities, assign a weight to distribute requests proportionally. A server with weight=3 receives three times as many requests as a server with weight=1:
upstream myapp_pool {
server 192.168.1.101:8080 weight=3;
server 192.168.1.102:8080 weight=2;
server 192.168.1.103:8080 weight=1;
}
In this configuration, out of every 6 requests, server 101 handles 3, server 102 handles 2, and server 103 handles 1. This is useful during a rolling upgrade where you want to gradually shift traffic to a new server.
Step 5: Least Connections (least_conn)
The least_conn algorithm routes each new request to the backend server with the fewest active connections at that moment. This is better than round-robin when requests vary significantly in processing time — for example, if some requests take milliseconds while others take several seconds, round-robin may pile requests up on a slow server while a fast server sits idle.
upstream myapp_pool {
least_conn;
server 192.168.1.101:8080;
server 192.168.1.102:8080;
server 192.168.1.103:8080;
}
Weights can be combined with least_conn. A server with higher weight is treated as if it has proportionally fewer active connections, biasing routing toward it:
upstream myapp_pool {
least_conn;
server 192.168.1.101:8080 weight=2;
server 192.168.1.102:8080 weight=1;
}
Step 6: IP Hash (ip_hash)
The ip_hash algorithm hashes the client’s IP address and consistently routes that client to the same backend server. This provides session persistence (sticky sessions) without requiring session storage to be shared across backends, which is useful for applications that store session data in memory on each server.
upstream myapp_pool {
ip_hash;
server 192.168.1.101:8080;
server 192.168.1.102:8080;
server 192.168.1.103:8080;
}
Important limitations of ip_hash: clients behind NAT all share the same source IP and will all be routed to the same backend. This can cause an uneven distribution. Also, removing a server from the pool will reassign a portion of hashed clients, temporarily breaking their sessions. To take a server out of rotation without disrupting existing hash mappings, use the down parameter:
upstream myapp_pool {
ip_hash;
server 192.168.1.101:8080;
server 192.168.1.102:8080;
server 192.168.1.103:8080 down;
}
Step 7: Passive Health Checking with max_fails and fail_timeout
Nginx open-source does not support active health checks (periodically probing backends to see if they are alive) — that feature requires Nginx Plus. However, it supports passive health checking: Nginx marks a backend as unavailable when it fails to respond successfully a certain number of times within a time window.
upstream myapp_pool {
server 192.168.1.101:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.102:8080 max_fails=3 fail_timeout=30s;
server 192.168.1.103:8080 max_fails=3 fail_timeout=30s;
}
max_fails=3: After 3 consecutive failed connection attempts or server errors within thefail_timeoutwindow, the server is marked unavailable.fail_timeout=30s: The window during which failures are counted, and also how long the server stays marked unavailable after reachingmax_fails. After 30 seconds, Nginx will try the server again with a single probe request.
The defaults are max_fails=1 and fail_timeout=10s. Setting max_fails=0 disables health checking for that server entirely, making it always considered available.
Step 8: Workaround for Active Health Checks (Open-Source Nginx)
Since active health checks are a Nginx Plus feature, the most common open-source workaround is to use an external health-check tool and update the upstream configuration when a server is down. However, a simpler in-config approach is to use the backup parameter to designate standby servers, combined with a monitoring script that flips servers between active and down states:
upstream myapp_pool {
server 192.168.1.101:8080 max_fails=2 fail_timeout=20s;
server 192.168.1.102:8080 max_fails=2 fail_timeout=20s;
# Backup server — only used when all primary servers are unavailable
server 192.168.1.103:8080 backup;
}
For a more robust active health check without Nginx Plus, install nginx_upstream_check_module by compiling Nginx from source with the patch applied, or use an external tool like HAProxy, Consul, or a custom shell script that polls backends and updates a dynamic upstream configuration file.
A simple shell script to check backend health and reload Nginx:
#!/bin/bash
BACKENDS=("192.168.1.101:8080" "192.168.1.102:8080" "192.168.1.103:8080")
CONF="/etc/nginx/conf.d/myapp-lb.conf"
for backend in "${BACKENDS[@]}"; do
if ! curl -sf --max-time 2 "http://${backend}/health" > /dev/null 2>&1; then
echo "$(date): Backend ${backend} is DOWN"
fi
done
Step 9: Configure SELinux for Network Connections
If your backend servers are on different hosts or ports not labeled with http_port_t, SELinux will block Nginx from connecting to them:
# Allow Nginx to connect to any network address
sudo setsebool -P httpd_can_network_connect 1
# Or label specific ports (preferred for tighter security)
sudo yum install -y policycoreutils-python
sudo semanage port -a -t http_port_t -p tcp 8080
Step 10: Test and Reload Nginx
sudo nginx -t
sudo systemctl reload nginx
Step 11: Verify Load Distribution
The simplest way to verify that requests are being distributed across backends is to watch the access logs on each backend server while sending repeated requests through Nginx:
# On the Nginx server, send 30 requests
for i in $(seq 1 30); do curl -s http://yourdomain.com/ > /dev/null; done
Check the access log count on each backend:
# On each backend server
sudo tail -30 /var/log/httpd/access_log | wc -l
For round-robin with three equal servers, you expect approximately 10 requests per server. For weighted round-robin with weights 3:2:1, expect approximately 15:10:5.
You can also use Nginx’s built-in status module to monitor upstream state at runtime (requires the --with-http_stub_status_module compile flag, which is included in the EPEL package):
location /nginx_status {
stub_status on;
allow 127.0.0.1;
deny all;
}
curl http://127.0.0.1/nginx_status
Nginx load balancing on RHEL 7 is entirely configuration-driven and requires no external software beyond the base Nginx package from EPEL. By choosing the right algorithm — round-robin for equal servers, least_conn for variable-length requests, ip_hash for sticky sessions — and pairing it with sensible max_fails and fail_timeout values for passive health checking, you can build a resilient, high-availability frontend that automatically routes around failed backends and distributes load proportionally across your server fleet. Adding weights and backup servers gives you additional operational flexibility for maintenance windows and capacity management without any downtime.