Nginx vs Caddy vs Traefik: Performance, Config, and Use Cases Compared
A hands-on comparison of Nginx, Caddy, and Traefik reverse proxies. Benchmarks, configuration complexity, SSL handling, Docker integration, and real-world...
The Reverse Proxy Decision
Every web application needs a reverse proxy. The question is which one. Nginx, Caddy, and Traefik approach the problem differently, and the right choice depends on your infrastructure and team.
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 170" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="170" rx="12" fill="#1a1a2e"/><text x="60" y="30" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui">Internet</text><circle cx="60" cy="60" r="25" fill="none" stroke="#3b82f6" stroke-width="1.5"/><text x="60" y="57" text-anchor="middle" fill="#3b82f6" font-size="18" font-family="system-ui">🌐</text><rect x="155" y="25" width="120" height="120" rx="10" fill="#6366f1" opacity="0.15"/><rect x="155" y="25" width="120" height="120" rx="10" fill="none" stroke="#6366f1" stroke-width="1.5"/><text x="215" y="50" text-anchor="middle" fill="#6366f1" font-size="11" font-family="system-ui" font-weight="bold">Reverse</text><text x="215" y="65" text-anchor="middle" fill="#6366f1" font-size="11" font-family="system-ui" font-weight="bold">Proxy</text><text x="215" y="85" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">TLS termination</text><text x="215" y="98" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">Load balancing</text><text x="215" y="111" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">Path routing</text><text x="215" y="124" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">Rate limiting</text><rect x="350" y="20" width="110" height="35" rx="6" fill="#2dd4bf" opacity="0.8"/><text x="405" y="42" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">app.example.com</text><rect x="350" y="65" width="110" height="35" rx="6" fill="#a855f7" opacity="0.8"/><text x="405" y="87" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">api.example.com</text><rect x="350" y="110" width="110" height="35" rx="6" fill="#f59e0b" opacity="0.8"/><text x="405" y="132" text-anchor="middle" fill="#1a1a2e" font-size="10" font-family="system-ui">cdn.example.com</text><defs><marker id="arrow11" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6" fill="#e2e8f0"/></marker></defs><line x1="87" y1="60" x2="153" y2="75" stroke="#e2e8f0" stroke-width="1.5" marker-end="url(#arrow11)"/><line x1="277" y1="55" x2="348" y2="37" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow11)"/><line x1="277" y1="85" x2="348" y2="82" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow11)"/><line x1="277" y1="115" x2="348" y2="127" stroke="#e2e8f0" stroke-width="1" marker-end="url(#arrow11)"/><text x="120" y="55" text-anchor="middle" fill="#2dd4bf" font-size="8" font-family="system-ui">HTTPS</text><text x="505" y="42" text-anchor="start" fill="#94a3b8" font-size="8" font-family="system-ui">:3000</text><text x="505" y="87" text-anchor="start" fill="#94a3b8" font-size="8" font-family="system-ui">:8080</text><text x="505" y="132" text-anchor="start" fill="#94a3b8" font-size="8" font-family="system-ui">:9000</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">A reverse proxy terminates TLS, routes requests by hostname, and load-balances across backend services.</p></div>
Nginx: The Battle-Tested Workhorse
Nginx has been the default reverse proxy for over 15 years. It handles massive scale, is extremely well-documented, and is the most performant option for static file serving.
# /etc/nginx/conf.d/app.conf
upstream api_backend {
least_conn;
server app1:3000 weight=5;
server app2:3000 weight=3;
server app3:3000 backup;
}
server {
listen 80;
server_name api.example.com;
return 301 https://api.example.com$request_uri;
}
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/letsencrypt/live/api.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
# Security headers
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";
# Gzip compression
gzip on;
gzip_types text/plain application/json application/javascript text/css;
gzip_min_length 1000;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://api_backend;
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;
# Timeouts
proxy_connect_timeout 5s;
proxy_read_timeout 30s;
proxy_send_timeout 30s;
}
location /static/ {
alias /var/www/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
}Caddy: Automatic HTTPS, Zero Config
Caddy's killer feature is automatic HTTPS. Point a domain at Caddy, and it obtains and renews TLS certificates from Let's Encrypt automatically. No certbot, no cron jobs, no manual renewal.
# Caddyfile
api.example.com {
reverse_proxy app1:3000 app2:3000 {
lb_policy least_conn
health_uri /health
health_interval 10s
}
encode gzip
header {
X-Frame-Options DENY
X-Content-Type-Options nosniff
Strict-Transport-Security "max-age=31536000; includeSubDomains"
-Server
}
log {
output file /var/log/caddy/api.log
format json
}
}
static.example.com {
root * /var/www/static
file_server {
precompressed gzip br
}
header Cache-Control "public, max-age=2592000, immutable"
}
# That's it. HTTPS is automatic. No certificate configuration needed.Traefik: Docker-Native Discovery
Traefik auto-discovers services from Docker labels. No config files to update when you add or remove services.
# docker-compose.yml
services:
traefik:
image: traefik:v3.6
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.letsencrypt.acme.tlschallenge=true
- [email protected]
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(`api.example.com`)"
- "traefik.http.routers.app.entrypoints=websecure"
- "traefik.http.routers.app.tls.certresolver=letsencrypt"
- "traefik.http.services.app.loadbalancer.server.port=3000"
# Rate limiting
- "traefik.http.middlewares.app-ratelimit.ratelimit.average=100"
- "traefik.http.routers.app.middlewares=app-ratelimit"<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="200" rx="12" fill="#1a1a2e"/><path d="M100,30 L500,30 L460,65 L140,65 Z" fill="#3b82f6" opacity="0.8"/><text x="300" y="53" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">Unoptimized Code — 2000ms</text><path d="M140,70 L460,70 L420,105 L180,105 Z" fill="#6366f1" opacity="0.8"/><text x="300" y="93" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">+ Caching — 800ms</text><path d="M180,110 L420,110 L380,145 L220,145 Z" fill="#a855f7" opacity="0.8"/><text x="300" y="133" text-anchor="middle" fill="#ffffff" font-size="11" font-family="system-ui">+ CDN — 200ms</text><path d="M220,150 L380,150 L350,175 L250,175 Z" fill="#2dd4bf" opacity="0.9"/><text x="300" y="168" text-anchor="middle" fill="#1a1a2e" font-size="11" font-family="system-ui" font-weight="bold">Optimized — 50ms</text><text x="530" y="53" text-anchor="start" fill="#94a3b8" font-size="10" font-family="system-ui">Baseline</text><text x="445" y="93" text-anchor="start" fill="#2dd4bf" font-size="10" font-family="system-ui">-60%</text><text x="405" y="133" text-anchor="start" fill="#2dd4bf" font-size="10" font-family="system-ui">-90%</text><text x="365" y="168" text-anchor="start" fill="#2dd4bf" font-size="10" font-family="system-ui" font-weight="bold">-97.5%</text></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Performance optimization funnel: each layer of optimization compounds to dramatically reduce response times.</p></div>
Performance Benchmarks
Tested with wrk (10 threads, 400 connections, 30 seconds) proxying to a simple JSON API:
|--------|-------|-------|---------|
Nginx wins on raw performance, especially for static files. Caddy and Traefik are comparable, with Caddy slightly ahead.
Configuration Complexity
Lines of config to achieve the same setup (reverse proxy + SSL + rate limiting + logging):
|------|-------|-------|---------|
Feature Comparison
|---------|-------|-------|---------|
<div style="margin:2.5rem auto;max-width:600px;width:100%;text-align:center;"><svg viewBox="0 0 600 200" xmlns="http://www.w3.org/2000/svg" style="width:100%;height:auto;"><rect width="600" height="200" rx="12" fill="#1a1a2e"/><rect x="15" y="10" width="570" height="25" rx="6" fill="#6366f1" opacity="0.3"/><circle cx="30" cy="22" r="4" fill="#ef4444"/><circle cx="42" cy="22" r="4" fill="#f59e0b"/><circle cx="54" cy="22" r="4" fill="#2dd4bf"/><text x="300" y="27" text-anchor="middle" fill="#ffffff" font-size="10" font-family="system-ui">Monitoring Dashboard</text><rect x="20" y="45" width="130" height="55" rx="6" fill="#6366f1" opacity="0.2"/><text x="85" y="65" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">CPU Usage</text><text x="85" y="88" text-anchor="middle" fill="#2dd4bf" font-size="18" font-family="system-ui" font-weight="bold">23%</text><rect x="160" y="45" width="130" height="55" rx="6" fill="#6366f1" opacity="0.2"/><text x="225" y="65" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Memory</text><text x="225" y="88" text-anchor="middle" fill="#f59e0b" font-size="18" font-family="system-ui" font-weight="bold">6.2 GB</text><rect x="300" y="45" width="130" height="55" rx="6" fill="#6366f1" opacity="0.2"/><text x="365" y="65" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Requests/s</text><text x="365" y="88" text-anchor="middle" fill="#6366f1" font-size="18" font-family="system-ui" font-weight="bold">1.2K</text><rect x="440" y="45" width="140" height="55" rx="6" fill="#6366f1" opacity="0.2"/><text x="510" y="65" text-anchor="middle" fill="#94a3b8" font-size="9" font-family="system-ui">Uptime</text><text x="510" y="88" text-anchor="middle" fill="#2dd4bf" font-size="18" font-family="system-ui" font-weight="bold">99.9%</text><rect x="20" y="110" width="560" height="80" rx="6" fill="#6366f1" opacity="0.1"/><text x="45" y="125" fill="#94a3b8" font-size="8" font-family="system-ui">Response Time (ms)</text><polyline points="40,170 80,155 120,160 160,140 200,145 240,135 280,150 320,130 360,125 400,140 440,120 480,115 520,125 560,110" fill="none" stroke="#6366f1" stroke-width="2"/><polyline points="40,170 80,155 120,160 160,140 200,145 240,135 280,150 320,130 360,125 400,140 440,120 480,115 520,125 560,110" fill="url(#chartGrad)" stroke="none" opacity="0.3"/><defs><linearGradient id="chartGrad" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#6366f1"/><stop offset="100%" stop-color="transparent"/></linearGradient></defs><line x1="40" y1="130" x2="560" y2="130" stroke="#e2e8f0" stroke-width="0.3" opacity="0.2"/><line x1="40" y1="150" x2="560" y2="150" stroke="#e2e8f0" stroke-width="0.3" opacity="0.2"/><line x1="40" y1="170" x2="560" y2="170" stroke="#e2e8f0" stroke-width="0.3" opacity="0.2"/></svg><p style="margin-top:0.75rem;font-size:0.85rem;color:#94a3b8;font-style:italic;line-height:1.4;">Real-time monitoring dashboard showing CPU, memory, request rate, and response time trends.</p></div>
Our Recommendation
Choose Nginx when: You need maximum performance, you serve lots of static files, you need Lua scripting or OpenResty, your team already knows Nginx, or you are running bare-metal without containers.
Choose Caddy when: You want the easiest HTTPS setup, you are running non-Docker workloads, you want a simple config file, or you are a small team that wants minimal operational burden.
Choose Traefik when: You run Docker or Kubernetes, you add/remove services frequently, you want automatic service discovery from container labels, or you want a built-in dashboard.
At TechSaaS, we run Traefik for our Docker infrastructure because automatic service discovery from labels means zero config changes when we deploy new services. We also use Nginx (as nginx:alpine) for static site serving behind Traefik — our company website is built with Next.js static export served by an Nginx container with Traefik handling the routing. Best of both worlds: Traefik for routing, Nginx for static performance.
Need help with cloud infrastructure?
TechSaaS provides expert consulting and managed services for cloud infrastructure, DevOps, and AI/ML operations.