HAProxy or Nginx as a Reverse Proxy: How I Choose
Nginx and HAProxy get compared constantly, and the honest answer is that there's a lot of overlap, both can terminate TLS, both can load balance across multiple backends, both can route based on hostname or path. For a huge number of setups, either one would work fine. But they were built with different primary jobs in mind, and that shows up in the details once a setup gets past "one app server behind a proxy."
Nginx: the default for a single app server
If there's one application server, maybe with a couple of static asset directories, and the proxy's job is mostly TLS termination plus routing requests to the right place, Nginx is the simpler choice and does it 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 config is doing three jobs at once, serving static files directly, terminating TLS, and proxying everything else to the app, and for a single backend, that's exactly the right amount of complexity. Nginx's config syntax is also something most developers have at least seen before, which matters when other people need to touch it.
HAProxy: the default once there's more than one backend
The moment there's more than one instance of the application behind the proxy, the questions change: which backends are healthy right now, how should load be distributed between them, and what happens to a request mid-flight if a backend dies. This is HAProxy's core job, and it shows in how directly it's expressed:
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 is actively polling each backend's health endpoint, and a backend that starts failing health checks gets taken out of rotation automatically, no manual intervention. HAProxy's stats page (stats directive, usually on its own port) gives a live view of which backends are up, how many connections each is handling, and response times per backend, which becomes genuinely useful once there's more than one server to keep track of.
Using both together
These aren't mutually exclusive, and a common setup for anything beyond a single server is HAProxy at the edge handling TLS termination and load balancing across multiple app servers, each of which runs Nginx to serve static files and proxy to the application process. HAProxy doesn't need to know anything about static files, and Nginx doesn't need to know anything about the other servers, each piece does the job it's best at.
The actual decision
If there's one backend and the main job is TLS termination plus basic routing, Nginx is simpler, more familiar to most people who'll maintain it, and entirely sufficient, adding HAProxy in front of a single backend adds a layer without adding much capability. If there's more than one backend instance and health-aware load balancing matters, even just two app servers for redundancy, HAProxy's load balancing and health check model is built for exactly that, and trying to replicate it in Nginx means reaching for third-party modules or more complex configuration than HAProxy needs for the same result.
The question isn't which tool is better. It's whether "which backend should this request go to, and is that backend even healthy" is a question your setup needs answered, once it is, HAProxy earns its place.