HAProxy or Nginx as a Reverse Proxy: How I Choose
Advertisement
Nginx and HAProxy get compared constantly. The honest answer is that there is a massive amount of overlap. Both can terminate TLS, load balance across multiple backends, and route traffic based on hostnames or paths. For a huge number of setups, either one works perfectly fine.
But they were built with very different primary jobs in mind. That difference becomes painfully obvious once your setup evolves past "one app server sitting behind a proxy."
Nginx: The default for a single app server
If you have exactly one application server, maybe paired with a couple of static asset directories, and the proxy's job is just TLS termination plus routing requests, Nginx is the simpler choice. It does this job extremely well:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /static/ {
root /var/www/example.com;
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
This configuration handles three jobs at once: serving static files directly, terminating TLS, and proxying everything else to the application. For a single backend, that is exactly the right amount of complexity. Additionally, almost every developer has seen Nginx syntax before, which matters a lot when you aren't the only person maintaining it.
HAProxy: The default once there's more than one backend
The moment you spin up a second instance of your application, the questions fundamentally change. Which backends are currently healthy? How should load be distributed? What happens to a request mid-flight if a backend suddenly dies?
This is HAProxy's core job, and it shows in how directly the configuration expresses it:
frontend web
bind *:443 ssl crt /etc/haproxy/certs/example.com.pem
default_backend app_servers
backend app_servers
balance roundrobin
option httpchk GET /healthz
server app1 10.0.0.11:3000 check
server app2 10.0.0.12:3000 check
server app3 10.0.0.13:3000 check
option httpchk GET /healthz means HAProxy actively polls each backend's health endpoint. If a backend starts failing those checks, HAProxy automatically pulls it out of rotation. Zero manual intervention required.
HAProxy's built-in stats page gives you a live, detailed view of exactly which backends are up, how many connections each one is handling, and the response times per backend. That observability is genuinely indispensable once you have more than one server to keep track of.
Using both together
These tools are not mutually exclusive. A very common architecture for scaling beyond a single server is placing HAProxy at the edge. HAProxy handles the TLS termination and load balancing across multiple application servers. Then, each application server runs Nginx locally to serve static files and proxy requests directly to the application process.
HAProxy doesn't need to know anything about static files. Nginx doesn't need to know anything about the overall server cluster. Each piece strictly does the job it is best at.
The actual decision
If you have one backend and your main goal is TLS termination plus basic routing, Nginx is simpler, more familiar, and entirely sufficient. Sticking HAProxy in front of a single backend adds an entire network layer without adding much capability.
But if you have more than one backend instance and health-aware load balancing actually matters (even if it's just two app servers for redundancy), HAProxy is built exactly for that. Trying to replicate HAProxy's robust health check model in Nginx means bolting on third-party modules or writing far more complex configurations than HAProxy needs for the exact same result.
The question isn't which tool is objectively better. The question is whether "which backend should this request go to, and is that backend even healthy?" is a problem your infrastructure needs answered. Once it is, HAProxy easily earns its place.
Advertisement