Load balancing distributes incoming client requests across multiple backend servers, preventing any single server from becoming a bottleneck and improving both throughput and fault tolerance. Nginx’s built-in load balancing is highly capable and available in the open-source version without any additional modules for most common use cases. On RHEL 8, you can configure Nginx to balance traffic using several algorithms including round-robin, least connections, and IP hash, and you can fine-tune health checking behavior through passive failure detection. This tutorial covers the full setup from the upstream block through load-balancing algorithm selection, server weights, passive health checks, and connection pooling.
Prerequisites
- A RHEL 8 server with
sudoaccess - Nginx installed from the RHEL 8 AppStream repository and running
- At least two backend application servers or processes listening on different ports (e.g.,
127.0.0.1:3000,127.0.0.1:3001,127.0.0.1:3002) - Port 80 (and optionally 443) open in
firewalld - SELinux enforcing mode (default on RHEL 8)
Step 1 — Enable SELinux Network Connect Permission
On RHEL 8, SELinux enforcing mode blocks Nginx from connecting to backend processes by default. Enable the required boolean before configuring the upstream block, or you will encounter 502 Bad Gateway errors.
sudo setsebool -P httpd_can_network_connect 1
getsebool httpd_can_network_connect
Step 2 — Define the upstream Block
The upstream block defines the pool of backend servers. By default, Nginx uses round-robin — distributing requests sequentially across all listed servers. Create a configuration file for your load-balanced application:
sudo nano /etc/nginx/conf.d/loadbalancer.conf
Add a basic upstream block and server block:
upstream myapp {
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://myapp;
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_http_version 1.1;
}
}
sudo nginx -t && sudo systemctl reload nginx
Step 3 — Select a Load-Balancing Algorithm
Nginx supports three load-balancing methods in the open-source version. Choose the one that best suits your application’s characteristics:
- Round-robin (default) — no directive needed; requests rotate evenly across servers
- Least connections (
least_conn) — routes the next request to the server with the fewest active connections, ideal for requests with variable processing time - IP hash (
ip_hash) — routes requests from the same client IP to the same backend server consistently, providing basic session persistence
upstream myapp {
least_conn;
server 127.0.0.1:3000;
server 127.0.0.1:3001;
server 127.0.0.1:3002;
}
To use IP hash instead, replace least_conn; with ip_hash;. Only one method directive may be present in an upstream block at a time.
Step 4 — Assign Server Weights
If your backend servers have different hardware capacities, assign weights to control the proportion of traffic each server receives. A server with weight=3 receives three times as many requests as a server with the default weight of 1.
upstream myapp {
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 weight=1;
}
Weights work with both round-robin and least_conn but are incompatible with ip_hash. Reload Nginx after any upstream block changes:
sudo nginx -t && sudo systemctl reload nginx
Step 5 — Configure Passive Health Checks
Passive health checking is available in open-source Nginx. When a server fails to respond or returns an error, Nginx marks it as temporarily unavailable for the duration specified by fail_timeout after reaching max_fails consecutive failures. Requests are not sent to that server during its unavailability window.
upstream myapp {
least_conn;
server 127.0.0.1:3000 weight=3 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 weight=2 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3002 weight=1 max_fails=3 fail_timeout=30s;
}
With max_fails=3 and fail_timeout=30s, a server that returns 3 consecutive errors within 30 seconds is removed from rotation for 30 seconds. Active health checks that probe backends on a schedule require Nginx Plus; passive checks are the open-source equivalent.
Step 6 — Configure keepalive Connections in the upstream Block
The keepalive directive in the upstream block specifies the number of idle persistent connections to each upstream server that Nginx keeps open in its worker process connection cache. This reduces the overhead of establishing new TCP connections for each proxied request and is especially beneficial under high traffic.
upstream myapp {
least_conn;
server 127.0.0.1:3000 weight=3 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 weight=2 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3002 weight=1 max_fails=3 fail_timeout=30s;
keepalive 32;
}
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://myapp;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
The proxy_http_version 1.1 and proxy_set_header Connection "" directives are required to enable keepalive connections to upstream servers; HTTP/1.0 does not support persistent connections. Validate and reload:
sudo nginx -t && sudo systemctl reload nginx
Conclusion
Nginx is now configured as a load balancer on your RHEL 8 server, distributing traffic across multiple backend instances using the least_conn algorithm with weighted servers and passive health checking. The keepalive connection pool reduces TCP handshake overhead, and the SELinux boolean grants Nginx the necessary permissions to reach backend services. This setup forms a solid foundation for horizontally scaling any HTTP-based application running on RHEL 8.
Next steps: How to Enable HTTP/2 with Nginx on RHEL 8, How to Configure Nginx as a Reverse Proxy on RHEL 8, and How to Secure Nginx with Let’s Encrypt on RHEL 8.