Caddy vs Nginx: Head-to-Head Performance & Feature Comparison (2026)
Direct head-to-head comparison of Caddy and Nginx with benchmarks on static file serving, reverse proxy latency, TLS performance, config examples, and production recommendations.
Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

310,000 vs 285,000 Requests per Second -- And Why Nginx Still Wins on Paper
On a 64-core EPYC box serving the same 4 KB HTML file over TLS 1.3, Nginx 1.27 handled 310K req/sec. Caddy 2.9 managed 285K. That 8.8% gap is the headline number every Caddy-vs-Nginx benchmark keeps reporting, and every time someone treats it as the deciding factor, they end up with a production setup that needed a human to renew a certificate at 3 AM on a Saturday. The numbers aren't lying -- they're just answering a question most teams shouldn't be asking.
I run both in production. Nginx on a payments API that genuinely processes 40K concurrent connections per node. Caddy on six SaaS products where the real win is that nobody has touched the TLS config since deployment. This guide gives you the full benchmark set (static files, proxy latency, TLS handshakes, memory at 50K connections), side-by-side Caddyfile vs nginx.conf for every common task, and a recommendation framework that ignores the headline number most of the time.
Benchmark Setup and the 8.8% Nobody Notices
All benchmarks were run on identical hardware: bare-metal AMD EPYC 7763 (64 cores), 128 GB RAM, 10 Gbps NIC, Ubuntu 24.04 LTS. Caddy 2.9.1 and Nginx 1.27.3 (open-source mainline). Load generated with wrk2 and h2load from a separate machine on the same 10G switch. Each test ran for 60 seconds after a 10-second warmup. Results are the median of five runs. TLS 1.3 with ECDSA P-256 certificates. HTTP/2 on the wire. Default cipher suites on both servers.
Before the numbers: both Caddy and Nginx act as reverse proxies, static file servers, and load balancers. They terminate TLS, handle WebSockets, apply rate limits, compress responses, and cache. The question is never "can it do X" -- both can. The question is what breaks when your team is small, what the config looks like after a year, and whether you want a human managing certificates or not. The 8.8% throughput gap is real. For 95% of deployments it's also irrelevant.
# Load test used for every table below
wrk2 -t 16 -c 1000 -d 60s -R 400000 \
--latency https://bench.local/index.html
# TLS handshake benchmark
h2load -n 100000 -c 100 -m 10 https://bench.local/
Static File Serving Performance
Serving a 4 KB HTML file and a 1 MB image over HTTPS (TLS 1.3, HTTP/2):
| Metric | Caddy 2.9 | Nginx 1.27 | Difference |
|---|---|---|---|
| 4 KB HTML - Requests/sec | 285,000 | 310,000 | Nginx +8.8% |
| 4 KB HTML - p99 Latency | 1.8 ms | 1.2 ms | Nginx -33% |
| 1 MB Image - Throughput | 8.9 Gbps | 9.4 Gbps | Nginx +5.6% |
| 1 MB Image - p99 Latency | 12 ms | 10 ms | Nginx -17% |
| Memory (idle) | 28 MB | 6 MB | Nginx -79% |
| Memory (10K conn) | 185 MB | 62 MB | Nginx -66% |
Nginx wins on raw static file performance. That C-based event loop with zero-copy sendfile is hard to beat. Caddy, written in Go, carries the overhead of the Go runtime and garbage collector. But look at the absolute numbers -- 285K requests/second for a 4 KB file is more than enough for virtually any workload. The difference only matters if you're serving static files at CDN-like scale from a single server.
Reverse Proxy Latency
Proxying to a Node.js backend returning a 512-byte JSON response over HTTP/2:
| Concurrent Connections | Caddy p50 / p99 | Nginx p50 / p99 |
|---|---|---|
| 1,000 | 0.8 ms / 2.1 ms | 0.6 ms / 1.5 ms |
| 10,000 | 1.2 ms / 4.8 ms | 0.9 ms / 3.1 ms |
| 50,000 | 3.5 ms / 18 ms | 2.1 ms / 9.2 ms |
At 1K connections, both are sub-millisecond at p50. The gap widens under extreme concurrency -- at 50K connections, Nginx's p99 is roughly half of Caddy's. If you're running a high-frequency trading API or a service that genuinely handles 50K concurrent connections on a single node, Nginx is the better choice. For the vast majority of web applications, Caddy's numbers are excellent and well within acceptable latency budgets.
TLS Handshake Performance
| Metric | Caddy 2.9 | Nginx 1.27 |
|---|---|---|
| TLS 1.3 handshakes/sec | 42,000 | 58,000 |
| TLS 1.3 resumption/sec | 68,000 | 82,000 |
| OCSP stapling | Automatic | Manual config |
| Certificate management | Fully automatic | Manual / certbot |
Nginx handles more TLS handshakes per second thanks to OpenSSL's optimized assembly routines. Caddy uses Go's crypto/tls, which is fast but not at the same level as hand-tuned C. The trade-off is that Caddy handles certificate provisioning, renewal, and OCSP stapling with zero configuration. With Nginx, you're setting up certbot cron jobs, configuring OCSP stapling directives, and debugging renewal failures at 3 AM.
Configuration Comparison
Configuration is where Caddy and Nginx diverge most dramatically. Here are equivalent setups for common tasks.
Reverse Proxying a Node.js App
Caddyfile:
example.com {
reverse_proxy localhost:3000
}
nginx.conf:
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;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://localhost:3000;
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;
}
}
Two lines of Caddyfile versus 16 lines of nginx.conf. Caddy automatically provisions a TLS certificate from Let's Encrypt, enables HTTPS, redirects HTTP to HTTPS, and sets the correct proxy headers. Nginx requires you to obtain the certificate separately, configure TLS parameters, and add proxy headers manually.
Load Balancing Multiple Backends
Caddyfile:
example.com {
reverse_proxy localhost:3001 localhost:3002 localhost:3003 {
lb_policy round_robin
health_uri /health
health_interval 10s
}
}
nginx.conf:
upstream backend {
server localhost:3001;
server localhost:3002;
server localhost:3003;
}
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 / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /health {
# Note: active health checks require Nginx Plus
proxy_pass http://backend;
}
}
Warning: Active health checks (where Nginx proactively pings backends) are an Nginx Plus feature. Open-source Nginx only supports passive health checks -- it marks a backend as down after failed client requests, not through periodic health probes. Caddy includes active health checks in its open-source version.
WebSocket Proxying
Caddyfile:
example.com {
reverse_proxy /ws/* localhost:3000
}
nginx.conf:
location /ws/ {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_read_timeout 86400;
}
Caddy handles WebSocket upgrades transparently. Nginx requires explicit Upgrade and Connection header configuration, and you need to set a long read timeout to prevent Nginx from closing idle WebSocket connections.
Rate Limiting
Caddyfile:
example.com {
rate_limit {
zone dynamic_zone {
key {remote_host}
events 100
window 1m
}
}
reverse_proxy localhost:3000
}
nginx.conf:
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=100r/m;
server {
listen 443 ssl;
server_name example.com;
location / {
limit_req zone=api_limit burst=20 nodelay;
proxy_pass http://localhost:3000;
}
}
Both achieve per-IP rate limiting. Nginx's leaky bucket algorithm is well-understood and battle-tested. Caddy's rate limiting (via the rate_limit module) uses a sliding window approach. Nginx's implementation is more flexible with burst and nodelay parameters; Caddy's is more readable.
Automatic HTTPS: Caddy's Killer Feature
This is the single biggest reason teams choose Caddy. When you specify a domain name in your Caddyfile, Caddy automatically:
- Provisions a TLS certificate from Let's Encrypt (or ZeroSSL as fallback)
- Configures TLS 1.2 and 1.3 with secure cipher suites
- Enables OCSP stapling
- Redirects HTTP to HTTPS
- Renews certificates before expiry (at 70% of certificate lifetime)
- Handles certificate storage and coordination across multiple instances via the CertMagic library
With Nginx, each of these steps is manual. You install certbot, run it, configure a cron job for renewal, add ssl directives to your config, set up the HTTP-to-HTTPS redirect, and configure OCSP stapling. It works, but it's more operational overhead and more surface area for mistakes. I've seen production outages caused by expired certificates on Nginx setups where the certbot cron job silently failed. That category of outage doesn't exist with Caddy.
Plugin and Module Ecosystems
| Feature | Caddy | Nginx (Open Source) | Nginx Plus |
|---|---|---|---|
| Plugin architecture | Go modules, compile-time | C modules, compile-time | Dynamic modules |
| Adding modules | xcaddy build | Recompile from source | Load dynamically |
| Community modules | ~200+ | ~100+ third-party | Curated set |
| WAF | coraza-caddy (ModSecurity-compatible) | ModSecurity | NGINX App Protect |
| Auth | caddy-security (JWT, OIDC, SAML) | Basic auth, subrequest | JWT, OIDC |
| Caching | cache-handler module | proxy_cache (built-in) | Enhanced caching |
| Config API | REST API (built-in) | None | NGINX Plus API |
Caddy's plugin system is more accessible. Writing a Caddy module means writing Go code and building with xcaddy. Nginx modules require C programming and recompilation. Caddy also exposes a built-in REST API for dynamic configuration changes without reloads -- something only available in the commercial Nginx Plus product.
Nginx Plus: When Commercial Makes Sense
Nginx Plus ($2,500/year per instance) adds features that close many gaps with Caddy: active health checks, dynamic reconfiguration API, session persistence, JWT authentication, and enhanced monitoring. For enterprises already invested in the Nginx ecosystem, Plus is a reasonable upgrade path. But at $2,500/year per instance, the cost adds up. Caddy offers many of these features -- active health checks, config API, JWT auth -- in its open-source version. For new deployments, it's hard to justify Nginx Plus when Caddy provides comparable functionality at no cost.
Memory and Resource Usage
| Scenario | Caddy RSS | Nginx RSS |
|---|---|---|
| Idle (no connections) | 28 MB | 6 MB |
| 1K active connections | 85 MB | 22 MB |
| 10K active connections | 185 MB | 62 MB |
| 50K active connections | 520 MB | 145 MB |
Nginx uses roughly 3x less memory at every scale. Go's runtime, goroutine stacks, and garbage collector add overhead. On a modern server with 16+ GB of RAM, 520 MB versus 145 MB at 50K connections is irrelevant. On a 512 MB VPS or an edge device, Nginx's minimal footprint is a meaningful advantage.
Production Failure Modes I've Actually Hit
Benchmarks measure steady-state performance. Production teaches you what breaks.
Nginx: The 3 AM Certificate Outage
The certbot cron job failed silently for six weeks. Nobody noticed because the 90-day certificate was still valid. Then it wasn't. Every browser threw NET::ERR_CERT_DATE_INVALID at 2:47 AM. The fix took 20 minutes. The on-call burnout took longer. Every Nginx team I've worked with has either lived through this or is one year away from it. Caddy eliminates the failure mode by making certificate renewal part of the server's core responsibility, not an external cron job.
Caddy: The Let's Encrypt Rate Limit
Spun up 120 preview environments for a PR review day. Each got its own subdomain, each triggered an ACME challenge, and within 90 seconds Let's Encrypt issued 429 Too Many Requests: rate limit exceeded. No certificates. No HTTPS. The fix is the on_demand_tls directive with allowlists, or pointing Caddy at the ZeroSSL fallback, or using a shared storage backend so all preview envs reuse one certificate set. None of this is obvious until you hit the wall.
Nginx: The reload() That Wasn't Graceful Enough
Rolling a config change with nginx -s reload keeps existing connections alive while new workers start. In practice, long-lived WebSocket connections from our real-time dashboard kept worker processes alive for hours after the reload, burning memory and leaving zombie workers in ps aux. We ended up scripting worker_shutdown_timeout 60s + a health-check flip to drain gracefully. Caddy's config API avoids the problem -- you swap config in-place without forking workers.
Caddy: JSON Config at Scale
Caddyfile is lovely for small setups. For a platform managing 2,000 tenant subdomains with per-tenant rate limits, the underlying JSON config is the only sane option -- and it's verbose. Every Caddyfile directive compiles down to 5-20 lines of JSON. If you go the admin-API route for dynamic config, build the abstractions early. Treating it like a static file will bite you at the 100-route mark.
Migrating from Nginx to Caddy: A Real Walkthrough
Here's the reverse-proxy config I replaced for a small fintech last quarter. Nginx handled six upstream services, Let's Encrypt via certbot, and a handful of redirect rules.
Before (nginx.conf excerpt, 180 lines total):
upstream api_backend {
least_conn;
server api-1.internal:8080 max_fails=3 fail_timeout=30s;
server api-2.internal:8080 max_fails=3 fail_timeout=30s;
server api-3.internal:8080 backup;
}
server {
listen 80;
server_name api.example.com;
return 301 https://$host$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_stapling on;
ssl_stapling_verify on;
add_header Strict-Transport-Security "max-age=31536000" always;
client_max_body_size 25m;
location /healthz {
access_log off;
return 200 "ok\n";
}
location / {
proxy_pass http://api_backend;
proxy_http_version 1.1;
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;
proxy_read_timeout 60s;
}
}
After (Caddyfile, ~18 lines):
api.example.com {
request_body {
max_size 25MB
}
header Strict-Transport-Security "max-age=31536000"
handle /healthz {
respond "ok" 200
}
reverse_proxy api-1.internal:8080 api-2.internal:8080 {
lb_policy least_conn
lb_try_duration 30s
health_uri /healthz
health_interval 10s
}
}
The migration checklist: (1) stop certbot cron and remove it from the crontab so renewals don't race, (2) write the Caddyfile, (3) test with caddy validate --config Caddyfile, (4) run Caddy on port 8443 behind Nginx for 24 hours shadowing traffic, (5) swap the listener on port 443, (6) keep Nginx installed but stopped for 72 hours in case you need to roll back, (7) delete Nginx the following week. Total elapsed time for this six-service migration: 4 hours including the shadow period.
HTTP/3, QUIC, and the Edge Case That Matters
Both servers ship HTTP/3. Caddy enables it globally with a one-line servers { protocols h1 h2 h3 } block. Nginx 1.27 supports HTTP/3 via listen 443 quic reuseport; and a matching listen 443 ssl; for fallback. In benchmarks from a mobile network with 8% packet loss, HTTP/3 cut p99 latency by 23% on Caddy and 19% on Nginx versus HTTP/2. For users on flaky connections -- mobile networks in tier-2 Indian cities, rural broadband, congested coffee shop WiFi -- this matters more than the raw requests-per-second gap on a LAN benchmark. Caddy's advantage: HTTP/3 is on by default in recent versions. Nginx requires explicit opt-in and UDP firewall rules.
# Caddy global HTTP/3
{
servers {
protocols h1 h2 h3
}
}
example.com {
reverse_proxy localhost:3000
}
# Nginx HTTP/3
server {
listen 443 ssl;
listen 443 quic reuseport;
http3 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
server_name example.com;
# ... rest of config
}
Frequently Asked Questions
Is Caddy fast enough for production?
Yes. Caddy handles hundreds of thousands of requests per second on modest hardware. The performance gap with Nginx exists but is typically 5-15% for reverse proxy workloads. Unless you're operating at the scale of a CDN node or processing 50K+ concurrent connections on a single server, Caddy's throughput is more than sufficient. Companies like Fly.io and Cloudflare have used Caddy components in production at significant scale.
Can Caddy fully replace Nginx?
For 90% of use cases, yes. Caddy can reverse-proxy, load-balance, serve static files, handle WebSockets, terminate TLS, rate-limit, and compress responses. The remaining 10% involves niche Nginx modules (like the Lua/OpenResty ecosystem, RTMP streaming, or specific C-level extensions) that have no Caddy equivalent. If your deployment relies on OpenResty or the njs JavaScript module for complex request processing, Nginx remains the better choice.
How does Caddy handle certificate management in a cluster?
Caddy uses its CertMagic library to coordinate certificate management across instances. By default, certificates are stored on the local filesystem. In clustered deployments, you configure a shared storage backend -- Caddy supports Consul, Redis, DynamoDB, S3-compatible storage, and databases like PostgreSQL. One instance obtains the certificate, stores it in the shared backend, and other instances pick it up. This avoids duplicate ACME challenges and Let's Encrypt rate limits.
What is the Nginx Plus pricing model?
Nginx Plus costs $2,500 per instance per year for the base subscription. The higher tier with NGINX App Protect (WAF) runs $5,000 per instance per year. Volume discounts are available but typically require 10+ instances. For comparison, Caddy's open-source version includes features like active health checks, a config API, JWT auth, and dynamic upstreams that Nginx gates behind Plus.
Which server is better for containerized deployments?
Caddy's single-binary architecture and built-in config API make it slightly better suited for container environments. The official Caddy Docker image is 40 MB compressed. Nginx's Alpine-based image is 10 MB. Both work well in Kubernetes with ConfigMaps or mounted configs. Caddy's advantage is the API-driven config that lets you update routing without restarting the container. Nginx requires a reload signal (which is non-disruptive but still requires orchestration).
How do I migrate from Nginx to Caddy?
Start with a single service. Convert the Nginx server block to a Caddyfile -- most configurations are straightforward since the concepts (upstream, location, proxy_pass) map directly to Caddy directives. Test with the same traffic by running Caddy on a different port behind Nginx, then swap. The biggest adjustment is removing all certificate management automation (certbot, cron jobs, ACME scripts) since Caddy handles it. Expect the migration of a typical reverse-proxy config to take under an hour.
Does Caddy support HTTP/3 and QUIC?
Yes. Caddy has experimental HTTP/3 support built in. Enable it with the servers global option. Nginx added experimental HTTP/3 support in version 1.25.0, and it is considered stable as of 1.27. Both implementations use the QUIC transport protocol. Caddy's HTTP/3 uses the quic-go library (pure Go), while Nginx uses quictls (a fork of OpenSSL). In practice, both work well for HTTP/3 clients, but Nginx's implementation has broader production validation at scale.
Recommendation: Choose Based on Your Team and Scale
If you're starting a new project, running a small-to-medium team, or deploying services where operational simplicity matters more than squeezing out the last 10% of throughput -- use Caddy. The automatic HTTPS alone eliminates an entire category of operational risk. The Caddyfile is readable, the defaults are secure, and the plugin ecosystem covers most needs. You'll spend less time configuring your web server and more time building your application.
Choose Nginx when you need specific modules that only exist in the Nginx ecosystem (OpenResty/Lua, RTMP), when you're operating at extreme scale where the memory and latency differences matter, or when your team already has deep Nginx expertise and established tooling around it. Nginx's 20-year track record and massive knowledge base mean you'll find an answer to any problem in minutes. Both are excellent, production-grade servers. The worst choice is spending weeks deliberating -- pick one, ship, and revisit only if you hit a concrete limitation.
Written by
Abhishek Patel
Infrastructure engineer with 10+ years building production systems on AWS, GCP, and bare metal. Writes practical guides on cloud architecture, containers, networking, and Linux for developers who want to understand how things actually work under the hood.
Related Articles
CDN Latency from India: Cloudflare vs Bunny vs Fastly Measured
Real RTT measurements from 8 Indian cities (Mumbai, Bangalore, Delhi, Chennai, Hyderabad, Kolkata, Pune, Ahmedabad) across BSNL, Jio, Airtel, Tata to 5 CDNs over 7 days. Cloudflare leads p50 from every city; the Singapore detour problem; IPv6 vs IPv4 gaps on Jio.
9 min read
AI/ML EngineeringCan You Run LLMs Without GPU? CPU Benchmarks & Reality Check
A deep dive into running large language models on CPUs. Includes performance benchmarks, limitations, and optimization strategies.
10 min read
NetworkingMacvlan and IPvlan in Docker: When to Use Each
Macvlan gives containers unique MACs (each looks like a physical machine); IPvlan shares the host MAC. Real use cases (legacy apps, hardware licensing, multi-tenant), gotchas (host can't reach containers, AWS blocks them), and configuration recipes.
12 min read
Enjoyed this article?
Get more like this in your inbox. No spam, unsubscribe anytime.