Pillar guide · INFRASTRUCTURE · 11 min read

Reverse proxies compared: nginx, Caddy, HAProxy and Traefik

A reference for picking and configuring the four reverse proxies that actually matter in production today.

Reverse proxies compared: nginx, Caddy, HAProxy and Traefik
Illustration · HostDir Editorial

A reverse proxy sits between the public internet and your application. It terminates TLS, routes requests, applies rate limits, and rewrites paths. Four projects dominate the space in 2026: nginx, Caddy, HAProxy and Traefik. Each is good at a different shape of job. This guide compares them honestly on config model, TLS handling, observability, hot reload and operational cost, and tells you which one to pick for which job.

What a reverse proxy actually does

A reverse proxy is a server that accepts incoming HTTP or HTTPS connections from the public internet and decides where to forward them. From the client's point of view, the reverse proxy is the website. From your application's point of view, the reverse proxy is a polite intermediary that hands over requests already authenticated, decompressed and routed.

It is doing several jobs at once. The four to keep clearly separated in your head:

  • TLS termination. The proxy holds the certificates, decodes HTTPS, and talks to the backend over plain HTTP on a loopback or trusted network. The backend application never touches a public certificate.
  • Routing. Different paths, hostnames or methods get sent to different backends. The proxy is the single front door, the application servers behind it are private rooms.
  • Policy. Rate limits, IP allow lists, header injection, security headers, request size limits, response compression. Anything that should apply to all backends lives at the proxy.
  • Observability. Access logs, error logs, metrics. A reverse proxy is the cleanest place to instrument because every request and response passes through it.

A load balancer is a related but different idea. A reverse proxy can do load balancing, but its primary job is the single-point routing and policy enforcement above. Load balancers proper care more about evenly distributing connections across a pool than about terminating TLS or rewriting URLs. We come back to this in the last section.

The four contenders at a glance

Every reverse proxy comparison written between 2010 and 2020 was essentially nginx versus Apache. That argument is over; nginx won and Apache moved on. The 2026 list is different. Four projects matter for real production work today:

  • nginx. The incumbent. Battle-tested, extremely well documented, ecosystem of everything. File-based config in a custom DSL. Open source plus an enterprise edition (F5 acquired Nginx Inc. in 2019).
  • Caddy. Newer, opinionated. Automatic HTTPS by default. Config in a custom Caddyfile syntax that compiles to JSON. Built in Go, single binary, easy to embed.
  • HAProxy. The layer-4 and layer-7 specialist. The reverse proxy you reach for when raw throughput, connection counts, and TCP-level load balancing dominate. Config is its own DSL, denser than nginx.
  • Traefik. Container-native. Discovers backends automatically from Docker labels, Kubernetes ingress, Consul, etcd and friends. Built in Go, single binary, designed for a world where backends come and go.
Editorial · HostDir

The right way to read the rest of this guide is not "which one wins" but "which one fits the shape of what I am running." All four can serve a static site, terminate TLS, and forward to a backend. The differences are in the operational ergonomics around that core capability.

nginx, the incumbent

nginx ships in every Linux distribution, runs in every PaaS, and is what you fall back to when you do not want to argue with anyone. The reasons it is still the default in 2026 are unsexy and good ones: it is stable, the documentation is excellent, every problem you will encounter has been encountered before and the answer is on the first page of search results.

A minimal site config in nginx:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols       TLSv1.2 TLSv1.3;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

That is the boilerplate. Real-world configs grow to include rate limits, gzip, security headers, separate locations for static files, fastcgi_pass for PHP, websocket upgrades for the API, and any number of variations.

nginx does not do automatic HTTPS. You run Certbot or acme.sh on a cron job, those tools issue and renew the certificates, and nginx reloads to pick them up. The split is intentional. nginx's job is to terminate TLS; ACME is a separate problem and a separate tool.

Hot reload is via nginx -s reload, which forks new workers with the new config and gracefully drains the old. No connections are dropped. This is fast and reliable.

Pick nginx when your workloads are mostly static or PHP-style, your hosts are long-lived, and you want the option of finding an answer to any problem in seconds.

Caddy, the automatic-TLS-by-default opinion

Caddy is what you reach for when you want HTTPS to take care of itself. Out of the box, given a server with public DNS and outbound network access, Caddy obtains certificates from Let's Encrypt (or ZeroSSL), renews them, and serves them. The config to make this happen is a single line:

example.com {
    reverse_proxy 127.0.0.1:8080
}

That is the entire Caddyfile. TLS, HTTP-to-HTTPS redirect, HTTP/2 and HTTP/3, and ACME enrollment all happen because Caddy assumes you want them. You can add headers, security policies, rate limits and routing rules, but the floor is much lower than nginx.

The Caddyfile syntax is its own thing. Internally it compiles to a JSON config that the Caddy server consumes via an admin API. Most users never touch the JSON. The Caddyfile is enough for almost everything.

Caddy reloads on caddy reload, which submits the new config to the admin API. Like nginx, it drains the old config without dropping connections.

The trade-off is that Caddy is opinionated. If you need to do something the Caddy maintainers have not considered worth supporting in the Caddyfile, you either drop to JSON or write a plugin. The plugin ecosystem is healthy but smaller than nginx's module ecosystem.

Pick Caddy when you have a single-digit number of domains, you want HTTPS to be invisible, and you trust opinionated defaults more than you fear edge cases. Most small site operators should use Caddy and stop worrying about Certbot.

HAProxy, the layer-4 specialist

HAProxy is what you put in front of the other three when you need real connection distribution at the edge. Its sweet spot is TCP load balancing, dense layer-7 routing across many backends, and any workload where the connection count and request rate dwarf what a single-machine reverse proxy can handle.

Editorial · HostDir

A minimal HAProxy config:

global
    daemon
    maxconn 50000

defaults
    mode http
    timeout connect 5s
    timeout client  30s
    timeout server  30s

frontend public
    bind :443 ssl crt /etc/haproxy/certs/
    bind :80
    http-request redirect scheme https unless { ssl_fc }
    default_backend app

backend app
    balance roundrobin
    server app1 10.0.0.10:8080 check
    server app2 10.0.0.11:8080 check
    server app3 10.0.0.12:8080 check

HAProxy expects you to manage certificates yourself; it has no built-in ACME client (though plugins exist). The config syntax has a learning curve. Frontend, backend, listen, defaults: you spend more time understanding the model than with nginx or Caddy, and the reward is correspondingly more direct control over connection behaviour.

HAProxy is also where you go for sophisticated health checking, sticky sessions, weighted routing, and traffic shaping. It is the right tool when you can articulate why those features matter for your case. It is the wrong tool when you cannot.

Pick HAProxy when you are doing real layer-4 work, you have many backends to balance, or you need health checking and connection control beyond what nginx's upstream module offers.

Traefik, the container-native option

Traefik is the reverse proxy designed around the world in which your backends come and go on their own schedule. A Docker container starts up; Traefik notices, reads the labels you attached to it, configures itself to route traffic to the container, and obtains a certificate. The container exits; Traefik tears the route down.

A docker-compose example showing the pattern:

services:
  traefik:
    image: traefik:v3.2
    command:
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "--certificatesresolvers.le.acme.email=ops@example.com"
      - "--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"
      - "--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./letsencrypt:/letsencrypt"

  app:
    image: my-app:latest
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(\`example.com\`)"
      - "traefik.http.routers.app.tls.certresolver=le"

The app container declares its hostname via a label; Traefik reads the label and routes accordingly. Add more services with their own labels and Traefik picks them up without restart.

Outside containers, Traefik can use file-based config, watch Kubernetes ingress, or pull config from etcd, Consul or ZooKeeper. The model is the same: declare what you want, Traefik watches for it, routes are dynamic.

The trade-off is that Traefik configuration spans wherever your service-discovery data lives. Debugging "why is this route not working" can involve checking labels, the Traefik dashboard, the entry point config, the certificate resolver, and the dynamic config provider, instead of one file you can grep.

Pick Traefik when your workloads run in containers, especially in numbers, and the friction of writing a static reverse proxy config every time a service is added is worse than the cost of dynamic discovery.

Picking one for a specific job

Five concrete shapes that come up often, and the right answer for each:

  • A single small site, want it to be HTTPS. Caddy. The five-line Caddyfile and automatic ACME are exactly what this job needs.
  • A WordPress or PHP-FPM site you are going to grow. nginx. You will want the fastcgi config, the static-file handling, the rewriting rules that nginx has worked out over fifteen years.
  • An API backed by Docker containers that come and go. Traefik. The label-driven discovery removes a category of human error.
  • A high-traffic edge in front of three internal nginx hosts. HAProxy on the edge for connection distribution, nginx on each internal host for site-specific routing.
  • A Kubernetes ingress controller. Traefik, or nginx-ingress, or any of the half-dozen mesh-adjacent options. Picking inside Kubernetes is a different question and outside this guide.

Config patterns common to all four

Whichever proxy you pick, the same patterns recur. Worth knowing as cross-cutting concepts rather than syntax in any one tool.

Trust the right headers

The backend application sees the request as coming from the proxy. To know the real client IP and protocol, the proxy needs to inject X-Forwarded-For, X-Forwarded-Proto and X-Real-IP, and the application needs to be configured to trust them. Trust them blindly and you let any client spoof headers; trust them only when the request came from the proxy's IP and you have safe origin information.

Always redirect HTTP to HTTPS

An HTTP-only response that says "go to HTTPS" is one line in every reverse proxy. It is also the difference between users landing on a usable site and a browser warning.

Compress the right things

gzip and brotli are now standard. Compress HTML, JSON, CSS, JS, SVG and similar text. Do not compress already-compressed formats (images, video, archives). Configure your compression level once and leave it.

Security headers

Strict-Transport-Security, X-Content-Type-Options, Referrer-Policy, and a sane Content-Security-Policy. The reverse proxy is the right place to set these globally so the backend cannot accidentally forget.

Operational concerns

Hot reload

All four support reloading config without dropping connections. The mechanics differ. nginx and HAProxy fork new workers and drain old ones. Caddy submits new config to its admin API. Traefik watches the config provider continuously.

Observability

nginx and HAProxy emit access logs and stats; Caddy and Traefik expose metrics on a Prometheus endpoint out of the box. For nginx and HAProxy, plugging in nginx-prometheus-exporter or haproxy-exporter closes the gap.

Certificate lifecycle

Caddy and Traefik handle ACME natively. nginx and HAProxy rely on external tools. The external-tool path is less magical and easier to audit, which some operators prefer. The native-ACME path is less work, which others prefer.

Config storage and versioning

Static configs (nginx, HAProxy, Caddyfile) live in git happily. Dynamic configs (Traefik labels, Kubernetes ingress) live wherever the service-discovery source lives. The difference matters for review and rollback.

When you actually need a load balancer behind it

Most reverse proxy configs end with proxy_pass http://127.0.0.1:8080 or similar: one backend, one machine. That is fine. The moment you have more than one backend, you are doing load balancing, even if you have not noticed.

nginx can balance across an upstream block with round-robin, least-connections or IP-hash. Caddy supports the same patterns. HAProxy was built for this and supports far more sophisticated strategies. Traefik balances across discovered backends automatically.

Editorial · HostDir

You only need a dedicated load balancer (HAProxy as a layer-4 edge, or a managed cloud LB) when the connection rate or backend count exceeds what a single reverse proxy can comfortably handle, or when you need TCP-level distribution for non-HTTP traffic. For a website with two backend app servers, an nginx upstream block is enough. For an API hit by ten thousand connections per second across forty backends, an HAProxy edge in front of the application proxies is the pattern.

The mistake is reaching for a dedicated load balancer prematurely. The other mistake is refusing to consider one when the proxy in front of your app is visibly the bottleneck. The right answer is to measure first and to keep the architecture as simple as the traffic allows.

Frequently asked questions
Do I need a reverse proxy if my app already speaks HTTPS? Read

Usually yes. Even when the app terminates TLS itself, putting a reverse proxy in front gives you a centralised place to add rate limiting, observability, security headers, and a kill switch. The app can speak plain HTTP on a loopback socket and the proxy handles the public side.

Is nginx still the fastest? Read

For static files and simple HTTP routing the difference is small enough not to matter. All four can saturate a 10 Gbps NIC on modern hardware. The right question is what you are doing, not what is fastest in synthetic benchmarks.

Can I run two of these in series? Read

Yes, and it is common. HAProxy as the layer-4 edge for connection distribution, with Caddy or nginx behind it for layer-7 routing and TLS termination on individual hosts, is a textbook pattern.

Does Caddy really renew certificates by itself? Read

Yes. It speaks ACME natively, watches its certificate store, and renews before expiry without supervision. You still need DNS pointing at the box and outbound network reach to Let's Encrypt or your chosen CA.

Does Traefik run outside containers? Read

Yes. It runs as a normal binary and can use file-based config or pull config from etcd, Consul, ZooKeeper, Kubernetes ingress, or a dozen other backends. The container-native angle is what makes it shine, not what it requires.

Which one supports HTTP/3 today? Read

All four. nginx added it in 1.25, Caddy has had it since 2.6, HAProxy added experimental support in 2.7 and stable in 2.9, Traefik supports it via its HTTPS entry points. The flag names differ but the capability is there.

Is Envoy a fifth option I should consider? Read

Envoy is the engine inside many service meshes and managed load balancers. If you are reaching for a reverse proxy to put in front of a small web app, Envoy is overkill. If you are already running a service mesh, you are not picking a reverse proxy in the sense this guide means.

Glossary terms used in this guide
TLS HTTPS HTTP/2 HTTP/3 QUIC SSL ACME DNS SNI TCP load balancer reverse proxy
Continue reading
INFRASTRUCTURE · Updated Jun 2026

Object storage explained: S3, R2, B2 and self-hosted MinIO

A reference for picking an object storage provider, understanding the egress trap, and knowing the parts of the S3 API that actually matter.

RFC grounded
INFRASTRUCTURE · Updated Jun 2026

Kubernetes for sysadmins: when it is worth it, and when it is not

An honest reference for engineers who run servers today, written from outside the Kubernetes evangelism circle. What K8s actually buys you, what it does not, and the alternatives that deserve consideration first.

RFC grounded
PERFORMANCE · Updated Jun 2026

Content Delivery Networks: A Complete Guide

How CDNs make the web fast, how they actually work, and how to pick one

RFC grounded
SECURITY · Updated Jun 2026

DDoS Protection: A Complete Guide

How modern attacks work, what defences actually scale, and how to choose protection

RFC grounded

Who Is Online

In total there are 560 users online: 0 registered, 554 guests and 6 bots.

Bots: AhrefsBot Applebot Facebook Other Bot SemrushBot YandexBot

Users active in the past 15 minutes. Total registered members: 386