NGINX vs Caddy vs Traefik: The Reverse-Proxy Head-to-Head
Every request to a self-hosted application passes through a gatekeeper: the reverse proxy that terminates TLS and picks the backend. Three programs own this job — NGINX, Caddy, and Traefik — and they disagree about configuration, certificates, and whether the proxy should know your containers exist. Pick the wrong one and you get a second job: renewals at 2 a.m. and a config file nobody dares to touch.
What the three programs actually are
NGINX is a web server and reverse proxy serving production traffic since 2004. Its open-source build is configured with text files: server blocks, location blocks, and directives such as proxy_pass. Nothing is discovered; every route is written down.
Caddy is a web server whose headline feature is automatic HTTPS. Its documentation states the claim plainly: "Caddy was the first web server to use HTTPS automatically and by default." A domain name in the config is enough: Caddy obtains the certificate, renews it, and redirects HTTP to HTTPS with no extra tooling. Configuration is the Caddyfile, or JSON.
Traefik is a cloud-native edge router that watches providers — Docker, Kubernetes, or a file — and builds routers, services, and middleware from what it finds. A container labelled with a Host rule gets a route and a certificate.
The three disagree on who keeps routes and certificates correct: you (NGINX), the server (Caddy), or the orchestrator (Traefik).
The config model: count the lines before you count the features
Write the same thing three times: example.com proxied to an app on port 8000, with HTTPS.
NGINX, nine lines:
```
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
Without the two proxy_set_header lines the app sees the proxy's address instead of the visitor's.
HTTPS is a second block with ssl_certificate and ssl_certificate_key pointing at files from elsewhere: NGINX ships no certificate automation in the open-source build, so a tool such as Certbot obtains and renews them.
Caddy, three lines:
```
example.com {
reverse_proxy 127.0.0.1:8000
}
```
The certificate is included: the domain activates automatic HTTPS. The reverse_proxy directive carries load balancing, health checks, and header handling behind its defaults — twelve load-balancing policies and both active and passive health checks.
Traefik, seventeen lines across two places. The static configuration, written once:
```
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
certificatesResolvers:
myresolver:
acme:
email: ops@example.com
storage: acme.json
httpChallenge:
entryPoint: web
```
And the per-service labels on the container:
```
labels:
- "traefik.enable=true"
- "traefik.http.routers.app.rule=Host(
example.com)" - "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=myresolver"
- "traefik.http.services.app.loadbalancer.server.port=8000"
```
Seventeen lines looks worst until you add the second service: NGINX needs another nine-line block, Caddy another three-line block, and Traefik five more labels. At ten services the totals are roughly 90 lines for NGINX, 30 for Caddy, and 12 static plus 50 label lines for Traefik.
At N = 10: NGINX 90, Caddy 30, Traefik 62.
Automatic HTTPS: who issues, who renews, and when it breaks
TLS is where the three diverge hardest: only two treat certificates as their problem.
Caddy's automatic HTTPS has four conditions, stated in its documentation: the domain's A or AAAA record points at the server, ports 80 and 443 are open, the data directory is writable and persistent, and the domain appears in the config. Meet them and the certificate appears. The default issuers are Let's Encrypt and ZeroSSL; the challenges are HTTP-01 on port 80, TLS-ALPN-01 on port 443, and DNS-01, which needs no open ports but DNS provider credentials. If issuance fails, Caddy retries in the background, at most one day between attempts, for up to thirty days. Test against the staging endpoint: production rate limits can block issuance for up to a week.
Traefik's ACME support is explicit: you declare a certificate resolver with an email, a storage file (acme.json by default), and a challenge. Its documentation sets the defaults in the open: the CA server is the Let's Encrypt v2 directory, the default key type is RSA4096, and certificates last 2,160 hours — ninety days — with renewal starting thirty days before expiry, on day sixty:
Wildcard certificates need the DNS-01 challenge; HTTP-01 and TLS-ALPN-01 cannot issue them. Traefik stores every certificate in one JSON file, acme.json: lose it and every certificate is reissued at once; expose it and the account key goes with it. Back it up like a database.
NGINX issues nothing. The open-source documentation describes ssl_certificate as a path to a file, and the file arrives by whatever process you build around it — Certbot, an internal CA, or a commercial certificate. That is not a flaw; it is the design. The lifecycle is yours to audit, and yours to forget at 2 a.m. when a renewal hook fails silently.
The failure modes follow the design. Caddy fails when its data directory is not persistent. Traefik fails when acme.json is lost, or when two instances share one resolver. NGINX fails when the renewal process is a cron job nobody monitors. Each failure is an expiry, and each expiry is a site that stops loading.
Routing and middleware: matchers, priority, and the rule that wins
Traefik's routing model is the most explicit. A router matches requests with a rule built from matchers: Host, HostRegexp, Path, PathPrefix, PathRegexp, Method, Header, Query, and ClientIP, combined with AND, OR, and NOT. When two routers could match, the longest rule wins — priority equals the length of the rule string. The documentation's example: a HostRegexp rule 34 characters long outranks a Host rule 26 characters long, so the broader pattern catches the request first unless you set priority explicitly. Adding a character to a rule can silently promote it above a more specific route, so name your routers and set priority explicitly where order matters.
NGINX routes with location blocks, matched in a documented order: exact matches first, then longest prefix, then regular expressions in file order. The order is deterministic and visible in the file, so the route a request takes is the route you trace by reading downward. The cost is verbosity: every condition is a line you maintain.
Caddy routes with matchers inside route and handle blocks. Middleware follows the same split: Traefik chains named middleware onto routers — rate limits, basic auth, rewrites, redirects, circuit breakers — declared once and reused. NGINX and Caddy scatter equivalent directives through blocks, copied per site unless factored into includes.
Load balancing and health checks: the arithmetic of staying up
NGINX's default is weighted round robin: three servers with weights 5, 1, and 1 receive seven requests as five to the first server and one each to the others.
Caddy's weighted_round_robin works the same way: weights of 5 and 1 select the first upstream five times in a row before the second is selected once, a share of five-sixths, about 83.3%. Caddy's default policy is random, and retries are disabled by default — a failed request is not retried unless you enable lb_retries, lb_try_duration, and lb_try_interval. Other policies include least_conn, ip_hash, client_ip_hash, uri_hash, and a cookie policy that pins a client to an upstream.
NGINX marks a server unavailable after max_fails unsuccessful attempts within fail_timeout — the defaults are one failure in ten seconds — and keeps it out for the same ten seconds. That is aggressive for a slow backend: one timeout removes a healthy-but-slow server for ten seconds. Set max_fails to 3 and fail_timeout to 30 seconds for backends whose slow responses are normal. A single-server group has no failover target: with one backend, a marked-down server just returns errors.
Traefik balances across the servers its provider discovered, and health checks mark servers down. Traefik learns the server list from the provider, so a stopped container leaves rotation without a config edit, while NGINX and Caddy need the list written down and a reload to change it. Caddy closes part of that gap with dynamic upstreams from SRV or A/AAAA records, refreshed every minute.
Sizing the box: worker connections and cache keys
NGINX is the only one that asks you to size the process, and its documentation gives you the numbers. The default worker_connections is 512 per worker process. With four worker processes the ceiling is:
A proxied request holds two connections — client to NGINX, NGINX to upstream — so the practical ceiling for concurrent proxied requests is roughly half that, about 1,024, before keepalive reuse moves the number. Raise worker_connections to 2,048 and the same four workers hold 8,192 connections, about 4,096 concurrent proxied requests. With NGINX you can compute the ceiling before the box falls over.
Caching is priced in keys, not bytes. NGINX documents that one megabyte of keys_zone holds about 8,000 cache keys, so 100,000 cached responses need a 13 MB zone:
Neither Caddy nor Traefik publishes equivalent sizing arithmetic: they run on defaults. Our editorial analysis found no vendor benchmark comparing throughput head to head, so this article makes no throughput claim — the numbers above are configuration arithmetic you can reproduce, not measurements.
The decision table
| Workload | Pick | Why | Authority |
|---|---|---|---|
| One to five sites, one server | Caddy | Three lines per site, certificates included | Caddy automatic-HTTPS docs |
| High-traffic single box | NGINX | Explicit worker and cache arithmetic | NGINX core + proxy module docs |
| Ten-plus containerised services | Traefik | Routes from provider labels; no shared proxy file | Traefik routers + ACME docs |
| Kubernetes ingress | Traefik | Ingress and CRD providers are native | Traefik routers docs |
| Regulated traffic with audit trails | NGINX | Config is a reviewed file; nothing is discovered | NGINX core docs |
Who should not use each one
Caddy is the wrong pick when the proxy must be invisible to the application team: its automatic HTTPS writes certificates into its data directory, and a team that expects certificates as files in a known path will fight the design.
NGINX is the wrong pick when nobody on the team will own the configuration file. Explicitness without an owner rots: location blocks accumulate, renewals go unmonitored, and the file becomes the thing nobody dares to reload. If the team that deploys services is not the team that edits the proxy, NGINX bills you in coordination cost.
Traefik is the wrong pick for a single static site, or any deployment where the service list is fixed and small. Its providers, dynamic configuration, and dashboard are machinery for change; on a box whose services never change, that machinery is surface area. It is also wrong where the network team owns routing: its routes live in container labels, which the network team cannot see from their own tooling.
For the hosting decision, see the Hetzner vs Contabo vs OVH bare-metal and VPS pricing playbook. For the runtime behind the proxy, the Docker vs Podman vs nerdctl container runtime playbook decides what it fronts. When the proxy must sit behind a tunnel, the Cloudflare Tunnel vs Tailscale vs ngrok remote-access playbook covers that layer. To price the margin the proxy protects, use the Business Profit Margin Calculator.
Founder & CEO Insight — Enow A. Jovial
The migration: execution checklist
- Inventory the routes first. List every host, path, and header rule the current proxy serves, with its backend. A route you forget is an outage you schedule.
- Stand the new proxy up beside the old one. Same backends, different port; prove the config with curl before any DNS moves.
- Move TLS before traffic. Issue the certificates on the new proxy and confirm them while the old proxy still serves production.
- Cut one host, not all of them. Move the lowest-traffic host first, watch error rates for an hour, then move the rest in rising-traffic order.
- Set the health checks before the cutover. Confirm a stopped backend leaves rotation.
- Watch the certificate renewal, not just the issuance. Force a renewal in staging, or diary the first renewal date and check it the next morning.
The decision in advance
There is no fastest proxy here: no vendor publishes a head-to-head benchmark, and this article invents none. There is a cheapest proxy to operate, and the answer falls out of three questions: how many services, who owns the configuration, and who owns the certificates. Fewer than five services with nobody owning certificates: Caddy. High traffic on one box with an owner who reads config files: NGINX. Ten or more services that change weekly: Traefik. Answer those three questions and the proxy picks itself.