The first time I set up load balancing with Apache was because a client’s Node.js application kept falling over under moderate traffic spikes, and scaling to multiple app instances behind a single entry point was the obvious fix. mod_proxy, combined with mod_proxy_balancer, turned out to be more than capable of handling this without introducing an entirely separate load balancer into the stack. I’ve used the same approach many times since, for PHP-FPM pools, Node backends, and even mixed application stacks behind one Apache front end.
Here’s exactly how I set it up.
What Is mod_proxy
mod_proxy is Apache’s module for proxying requests — forwarding incoming requests to one or more backend servers and returning their response to the client. On its own, it handles basic reverse proxying. Combined with mod_proxy_balancer and a protocol-specific module (mod_proxy_http for HTTP backends, mod_proxy_fcgi for FastCGI, mod_proxy_wstunnel for WebSockets), it can distribute requests across multiple backend servers using various load-balancing algorithms.
This makes Apache capable of acting as both a web server and a reverse proxy / load balancer at the same time — genuinely useful when you don’t want to stand up a completely separate piece of infrastructure just for load balancing.
Prerequisites
- Apache installed and running
- Root or sudo access
- Two or more backend application servers/instances to balance across (these can be on the same machine on different ports, or separate machines entirely)
- Basic familiarity with virtual hosts
Step 1: Enable the Required Modules
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod proxy_balancer
sudo a2enmod lbmethod_byrequests
sudo systemctl restart apache2
On CentOS/RHEL, these modules typically ship with the base httpd package — just make sure they’re loaded in /etc/httpd/conf.modules.d/:
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
Verify:
apachectl -M | grep proxy
Step 2: Basic Reverse Proxy Setup (Single Backend)
Before jumping into load balancing, here’s the simplest possible reverse proxy config, useful in its own right:
<VirtualHost *:80>
ServerName example.com
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
This forwards every request to an app running on port 3000 (a Node.js app, for example) while preserving the original Host header.
Step 3: Configuring Load Balancing Across Multiple Backends
Now the actual load balancing setup. Here’s a config balancing traffic across three backend instances:
<Proxy "balancer://mycluster">
BalancerMember "http://127.0.0.1:3001"
BalancerMember "http://127.0.0.1:3002"
BalancerMember "http://127.0.0.1:3003"
ProxySet lbmethod=byrequests
</Proxy>
<VirtualHost *:80>
ServerName example.com
ProxyPreserveHost On
ProxyPass / balancer://mycluster/
ProxyPassReverse / balancer://mycluster/
</VirtualHost>
Apache now distributes incoming requests across all three backend instances based on the chosen algorithm.
Load Balancing Methods
mod_proxy_balancer supports a few different algorithms, and I pick based on the situation:
byrequests(default) — distributes based on request count, roughly round-robinbytraffic— distributes based on actual byte traffic volume, useful when requests vary a lot in size/costbybusyness— sends new requests to whichever backend currently has the fewest active requests, good for backends with variable response times
ProxySet lbmethod=bybusyness
For most applications with roughly uniform request costs, byrequests is a perfectly reasonable default. I switch to bybusyness when backend response times vary significantly (e.g., some requests trigger heavy processing and others don’t).
Weighted Load Balancing
If your backend servers aren’t identical in capacity, you can weight them:
<Proxy "balancer://mycluster">
BalancerMember "http://127.0.0.1:3001" loadfactor=3
BalancerMember "http://127.0.0.1:3002" loadfactor=1
ProxySet lbmethod=byrequests
</Proxy>
Here the first backend receives roughly three times the traffic of the second — useful when one server has significantly more CPU/RAM than the other.
Health Checks and Failover
You’ll want Apache to stop sending traffic to a backend that’s down. Add health check parameters:
<Proxy "balancer://mycluster">
BalancerMember "http://127.0.0.1:3001" retry=30
BalancerMember "http://127.0.0.1:3002" retry=30
ProxySet lbmethod=byrequests
</Proxy>
retry=30 tells Apache to wait 30 seconds before retrying a backend that was previously marked as failed. You can also designate a hot spare that only receives traffic if the primary backends are all down:
BalancerMember "http://127.0.0.1:3099" status=+H
Enabling the Balancer Manager (Optional but Useful)
Apache includes a built-in web UI for monitoring and adjusting the balancer live:
<Location "/balancer-manager">
SetHandler balancer-manager
Require ip 127.0.0.1
</Location>
I always restrict access to this tightly (internal IPs only, or behind auth) since it exposes internal infrastructure details and lets you change balancer state.
Sticky Sessions
Some applications require a client to keep hitting the same backend for the duration of their session (common with server-side session storage that isn’t shared/centralized). Use ProxySet with a session identifier:
<Proxy "balancer://mycluster">
BalancerMember "http://127.0.0.1:3001" route=node1
BalancerMember "http://127.0.0.1:3002" route=node2
ProxySet lbmethod=byrequests
ProxySet stickysession=ROUTEID
</Proxy>
This requires your application (or a cookie set at the proxy layer) to include a ROUTEID value matching the backend’s route identifier. Where possible, I actually prefer to design applications with centralized/shared session storage (Redis, database-backed sessions) to avoid needing sticky sessions altogether — it makes scaling and failover much cleaner.
Testing and Reloading
sudo apachectl configtest
sudo systemctl reload apache2 # Debian/Ubuntu
sudo systemctl reload httpd # CentOS/RHEL
Verifying Load Balancing Is Working
I add a temporary identifying response header (or endpoint) on each backend during setup, then hit the balanced URL repeatedly to confirm requests are actually being distributed:
for i in {1..10}; do curl -s https://example.com/backend-id; echo; done
Watching the responses rotate across backend identifiers confirms the balancer is doing its job.
Real-World Use Cases
- Scaling Node.js or Python apps: Running multiple instances of a single-threaded or GIL-limited app behind Apache to use multiple CPU cores.
- PHP-FPM pools: Distributing load across multiple FPM pool instances.
- Blue-green deployments: Gradually shifting traffic weight from an old version to a new one during a rollout.
- High availability: Automatically routing around a failed backend server without manual intervention.
Troubleshooting Tips
- 502 Bad Gateway: Usually means all backends in the balancer are marked down — check that the backend apps are actually running and listening on the expected ports.
- Uneven traffic distribution: Check your
loadfactorsettings and chosenlbmethod—byrequestswon’t account for backend response time differences. - Sessions breaking intermittently: Likely a sticky session misconfiguration, or an application that isn’t actually sharing session state and needs
stickysessionset up correctly. - Balancer manager not accessible: Double check the
Require iprestriction matches the IP you’re actually connecting from.
Common Mistakes to Avoid
- Not configuring health checks/retry, leaving a downed backend receiving traffic that just fails.
- Leaving the balancer-manager endpoint open to the public internet.
- Using sticky sessions as a permanent architecture decision instead of moving toward centralized session storage.
- Forgetting
ProxyPreserveHost On, which can break applications relying on the originalHostheader. - Not load testing the balanced setup before relying on it in production.
Security Best Practices
- Restrict
/balancer-managerto trusted internal IPs or behind authentication. - Ensure backend servers aren’t directly accessible from the public internet — only Apache should be able to reach them, ideally via a private network or firewall rules.
- Keep
mod_proxyand related modules updated as part of regular patching. - Use HTTPS between Apache and backends too if traffic crosses an untrusted network segment, not just between the client and Apache.
Performance Optimization
- Choose the load-balancing method that matches your actual traffic pattern (
bybusynessfor variable-cost requests,byrequestsfor uniform ones). - Tune
retryand connection timeout values based on realistic backend startup/recovery times. - Monitor backend response times and adjust
loadfactorweighting if servers have different capacities. - Consider connection pooling/keep-alive settings between Apache and backends to reduce connection overhead under high traffic.
Frequently Asked Questions
Q: Can mod_proxy load balance HTTPS backends? A: Yes, use https:// in your BalancerMember URLs and ensure Apache trusts the backend’s certificate (or configure SSLProxyVerify accordingly).
Q: Is mod_proxy a replacement for dedicated load balancers like HAProxy or Nginx? A: For many small-to-medium setups, yes, it’s genuinely capable. For very high-traffic or highly specialized load-balancing needs, a dedicated tool may offer more advanced features.
Q: What happens if all my backends go down? A: Clients receive a 502 Bad Gateway error until at least one backend becomes available again.
Q: Do I need sticky sessions? A: Only if your application stores session data locally on each backend instance rather than in shared/centralized storage. If you can move to shared session storage, you generally should.
Q: How do I monitor which backend is handling requests? A: Use the balancer-manager UI, or add a custom header/endpoint on each backend that identifies itself for testing purposes.
Summary and Key Takeaways
mod_proxy combined with mod_proxy_balancer turns Apache into a genuinely capable reverse proxy and load balancer, without needing separate infrastructure. To recap:
- Enable
proxy,proxy_http,proxy_balancer, and anlbmethodmodule. - Define backends inside a
<Proxy "balancer://...">block and reference it withProxyPass. - Choose a load-balancing method that fits your traffic pattern, and weight backends if their capacity differs.
- Configure health checks/retry so failed backends are automatically routed around.
- Secure the balancer-manager UI and keep backend servers off the public internet.
It’s a solid, well-tested option that’s saved me from standing up extra infrastructure more times than I can count.