Skip to content
Networking

Nginx vs Apache vs Caddy: Which Web Server Is Best?

A performance and usability comparison of popular web servers including benchmarks, configuration complexity, and real-world usage.

A
Abhishek Patel9 min read

Infrastructure engineer with 10+ years building production systems on AWS, GCP,…

Nginx vs Apache vs Caddy: Which Web Server Is Best?
Nginx vs Apache vs Caddy: Which Web Server Is Best?

145 MB vs 18 MB: The RAM Number That Decides the Stack

On an otherwise-idle 4-core VPS under 400 concurrent keep-alive connections, Apache 2.4 with the event MPM holds 145 MB resident. Nginx 1.25 on the same box holds 18 MB. Caddy 2.7 sits in the middle at 32 MB. That's an 8x gap between the extremes, and it is not a tuning artifact -- it is a direct consequence of Apache's process/thread model vs Nginx's event loop, and it explains why a $5 DigitalOcean droplet can serve 70k req/s on Nginx but chokes at 15k req/s on Apache.

I have deployed all three in production for the better part of a decade, from a single-box side project on Caddy to an Nginx cluster in front of a Kubernetes fleet doing 50,000+ req/s. The right answer is never "the fastest one." It is the one that matches your traffic shape, your team's operational muscle, and the amount of YAML you are willing to tolerate. This guide puts each server under the same wrk profile, compares the config for a real reverse-proxy scenario, and lays out the exact workloads where each wins.

Performance Benchmarks: Real Numbers

Before any opinion, the actual data. I ran these on a 4-core, 8 GB RAM VPS (Ubuntu 22.04, kernel 6.8) using wrk with 12 threads and 400 connections for 30 seconds. The backend was a 1 KB static HTML file for the first table and a warm Node.js JSON API for the second.

MetricNginx 1.25Apache 2.4 (event MPM)Caddy 2.7
Requests/sec78,40042,10065,200
Avg Latency5.1ms9.5ms6.1ms
P99 Latency12ms28ms15ms
Memory Usage18MB145MB32MB
Transfer/sec62MB33MB52MB

For reverse proxy to a Node.js backend (JSON API responses):

MetricNginxApacheCaddy
Requests/sec24,60018,20022,800
Avg Latency16ms22ms17ms
P99 Latency35ms58ms38ms
CPU Usage45%72%52%

Pro tip: These benchmarks matter less than you think for most applications. If your backend responds in 200ms, the 1ms difference between Nginx and Caddy is noise. Focus on configuration simplicity and operational overhead unless you're serving 10,000+ requests per second.

What Each Server Actually Is

A web server listens for incoming HTTP/HTTPS requests, processes them according to configured rules, and returns responses -- static files from disk or proxied responses from a backend. Modern servers also terminate TLS, load-balance across upstreams, cache responses, and route based on headers, methods, or paths. All three servers in this comparison do every item on that list. The architectures they use to get there are what explain the benchmark numbers above.

Nginx, Apache, and Caddy can all serve static files, reverse proxy, terminate TLS, and handle virtual hosting. The differences that actually matter in production are (1) connection model, (2) configuration language, and (3) how much operational work the server takes off your plate -- particularly around TLS.

Architecture: How Each Server Handles Connections

Nginx: Event-Driven, Non-Blocking

Nginx uses an asynchronous, event-driven architecture. A small number of worker processes each handle thousands of connections using epoll (Linux) or kqueue (BSD/macOS). There's no thread-per-connection overhead. This is why Nginx excels at high concurrency -- 10,000 simultaneous connections consume roughly the same memory as 100.

The worker count typically matches your CPU core count. Each worker runs an event loop that processes connections without blocking. This design makes Nginx exceptionally memory-efficient under load.

Apache: Process/Thread Model (with MPMs)

Apache's architecture depends on which Multi-Processing Module (MPM) you choose. The prefork MPM spawns one process per connection -- safe for non-thread-safe modules like mod_php but memory-hungry. The worker MPM uses threads within processes, better for concurrency. The event MPM (default since Apache 2.4) adds keep-alive handling similar to Nginx's approach.

Even with the event MPM, Apache's per-connection resource usage is higher than Nginx's. At 10,000 concurrent connections, you'll see meaningful differences in memory consumption.

Caddy: Go-Based with Goroutines

Caddy is written in Go and uses goroutines for concurrency. Each connection gets its own goroutine, which is lightweight (around 4KB initial stack) compared to OS threads. Go's runtime scheduler multiplexes goroutines onto OS threads efficiently. The result is performance that's competitive with Nginx for most workloads, though raw throughput at extreme scale still favors Nginx.

Failure Modes I Have Debugged in Production

Apache prefork blowing the swap budget

A legacy WordPress stack running Apache prefork + mod_php spiked to 220 workers during a traffic burst, each holding ~40 MB of PHP opcode cache and database handles. The box had 8 GB of RAM. It hit swap, the OOM killer started harvesting Apache children, and the site went down for 40 minutes. The fix was the event MPM plus PHP-FPM -- but the migration took a weekend because .htaccess rules baked into 11 years of plugin configuration do not port cleanly to location blocks.

Nginx 502 storm after an upstream restart

Default Nginx upstream keepalive is zero. Every request opens a fresh TCP+TLS connection to the backend. When a rolling deploy restarts one upstream, Nginx's cached DNS points at a dead socket and returns 502 Bad Gateway until the resolver TTL expires (default 30 seconds). Fix: set resolver 127.0.0.11 valid=10s;, enable keepalive in the upstream block, and add proxy_next_upstream to fail over silently.

Caddy rate-limited by Let's Encrypt

Caddy's automatic TLS is magic until you hit the Let's Encrypt rate limit: 50 certificates per registered domain per week. A multi-tenant SaaS spinning up 80 customer subdomains on launch day blew through the quota at noon. Caddy started returning plaintext HTTP and logging ACME failures. The fix was to move every subdomain under a single wildcard cert (which bypasses the per-name limit) and pre-warm the cert in staging.

Keep-alive connection exhaustion

Default Nginx worker_connections is 512. Under a keep-alive-heavy workload (think 100k idle mobile clients), that 512 fills in minutes and new connections get refused. Bump worker_connections to 16384, raise the OS-level nofile limit to match, and verify with ss -s under load. Apache's equivalent is MaxRequestWorkers on the event MPM -- same rule, different knob.

Configuration Complexity: The Real Differentiator

Nginx Configuration

Nginx uses a custom configuration language with nested blocks. It's powerful but has a learning curve. Here's a basic reverse proxy with TLS:

server {
    listen 443 ssl http2;
    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://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

You'll also need a separate certbot setup for Let's Encrypt, a cron job for renewal, and careful attention to directive ordering. Configuration errors are silent until you reload.

Apache Configuration

Apache uses XML-like directives in httpd.conf or.htaccess files. The.htaccess feature lets you override configuration per-directory without restarting the server -- convenient for shared hosting, but a performance penalty on every request as Apache checks for.htaccess files in each directory.

<VirtualHost *:443>
    ServerName example.com
    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:3000/
    ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>

Caddy Configuration

Caddy's configuration is dramatically simpler. Automatic HTTPS is the default -- Caddy provisions and renews Let's Encrypt certificates with zero configuration:

example.com {
    reverse_proxy localhost:3000
}

That's it. Two lines. TLS is handled automatically. HTTP/2 is enabled by default. HTTP-to-HTTPS redirects happen automatically. This isn't a simplified example -- it's the actual production configuration.

TLS and HTTPS: A Clear Winner

Caddy wins the TLS story decisively. Here's what each server requires for HTTPS:

FeatureNginxApacheCaddy
Auto HTTPSNo (needs certbot)No (needs certbot)Yes, built-in
Auto RenewalCron job requiredCron job requiredAutomatic
HTTP/2Manual configManual config + mod_http2Default
HTTP/3 (QUIC)ExperimentalNoBuilt-in
OCSP StaplingManual configManual configAutomatic
Config Lines for TLS8-15 lines6-12 lines0 lines

Pricing and Cost Comparison

All three web servers are free and open source. The real costs are operational:

Cost FactorNginxApacheCaddy
LicenseFree (BSD-like)Free (Apache 2.0)Free (Apache 2.0)
Commercial VersionNginx Plus: $2,500/yrNoneNone
Memory per 10K conn~18MB~145MB~32MB
Setup Time (experienced)15-30 min20-40 min2-5 min
Ongoing MaintenanceMedium (cert renewal, tuning)Medium-High (.htaccess, modules)Low (auto-everything)
Server Cost SavingsLowest resource usageHighest resource usageModerate resource usage

Note: Nginx Plus adds features like active health checks, session persistence, JWT authentication, and a real-time dashboard. For most teams, the open-source version is sufficient. If you need those enterprise features, also evaluate HAProxy or Envoy as alternatives.

When to Use Each Web Server

Choose Nginx When

  1. High-traffic production environments -- Nginx handles extreme concurrency with minimal resources
  2. Complex routing and load balancing -- upstream blocks, weighted routing, and health checks are battle-tested
  3. You need the ecosystem -- Lua scripting (OpenResty), extensive third-party modules, massive community knowledge base
  4. Kubernetes ingress -- the Nginx ingress controller is the most widely deployed

Choose Apache When

  1. Shared hosting environments --.htaccess per-directory overrides are essential for multi-tenant setups
  2. Legacy PHP applications -- mod_php integration is still the simplest way to run WordPress, Drupal, etc.
  3. You need dynamic module loading -- Apache can load/unload modules without recompiling
  4. Existing infrastructure -- if your team already knows Apache deeply, switching has a real cost

Choose Caddy When

  1. Small to medium projects -- the configuration simplicity saves hours of setup and debugging
  2. You want automatic HTTPS -- no certbot, no cron jobs, no manual certificate management
  3. API servers and microservices -- drop-in reverse proxy with zero boilerplate
  4. Development environments -- instant local HTTPS with automatic self-signed certificates

Migration Steps: Switching Between Servers

  1. Audit your current configuration -- document every virtual host, rewrite rule, proxy pass, and custom header
  2. Map features to the target server -- most Nginx/Apache directives have direct Caddy equivalents
  3. Set up the new server alongside the old one -- run on a different port and test with curl
  4. Test TLS configuration -- use SSL Labs (ssllabs.com) to verify your grade matches or exceeds the original
  5. Load test before switching -- use wrk or k6 to ensure performance meets your requirements
  6. Switch DNS or update your load balancer -- point traffic to the new server with a quick rollback plan
  7. Monitor for 48 hours -- watch error rates, latency percentiles, and memory usage closely

Frequently Asked Questions

Is Nginx faster than Apache?

Yes, in most benchmarks Nginx outperforms Apache by 40-80% for static file serving and 20-35% for reverse proxying. The difference comes from Nginx's event-driven architecture versus Apache's process/thread model. However, with Apache's event MPM and proper tuning, the gap narrows significantly. For applications where backend processing dominates response time, the web server choice has minimal impact on end-user latency.

Can Caddy handle production traffic at scale?

Caddy handles production traffic well for most applications. Companies serving millions of requests per day use it successfully. However, at extreme scale (100,000+ requests per second), Nginx's C-based architecture gives it an edge in raw throughput and memory efficiency. For the vast majority of production workloads -- anything under 50,000 req/s -- Caddy performs comparably to Nginx with far less configuration overhead.

Does Caddy really handle TLS automatically?

Yes. When you specify a domain name in Caddy's configuration, it automatically obtains a Let's Encrypt certificate, configures HTTPS, sets up HTTP-to-HTTPS redirects, enables OCSP stapling, and handles renewal. There is zero TLS configuration required. For local development, Caddy generates and trusts self-signed certificates automatically. This feature alone saves hours of setup compared to Nginx or Apache.

Should I replace Apache with Nginx for WordPress?

It depends on your hosting setup. If you're on shared hosting, Apache with mod_php and.htaccess is the path of least resistance. For a dedicated server or VPS, Nginx with PHP-FPM is faster and uses less memory -- typically 30-50% less RAM under load. The migration requires converting.htaccess rewrite rules to Nginx location blocks, which can be tedious for complex WordPress configurations with many plugins.

What about Nginx Unit and OpenResty?

Nginx Unit is a separate application server (not a web server replacement) that natively runs Python, PHP, Go, Ruby, and Node.js applications. OpenResty extends Nginx with LuaJIT scripting, enabling complex request processing logic directly in the web server. Both are excellent tools, but they serve different purposes than a standard Nginx, Apache, or Caddy deployment.

Which web server is best for Kubernetes?

For Kubernetes ingress controllers, Nginx dominates with the most mature and widely deployed controller. Caddy has a growing Kubernetes ecosystem with the caddy-ingress-controller project, but it's less battle-tested. Apache is rarely used as a Kubernetes ingress controller. For serving applications inside containers, any of the three works -- Caddy's small binary and zero-config TLS make it attractive for sidecar patterns.

The Bottom Line

If you're starting a new project today and don't have specific requirements pushing you toward Nginx or Apache, use Caddy. The automatic TLS, minimal configuration, and solid performance make it the pragmatic choice for 80% of use cases. Switch to Nginx when you hit scale limits or need its deeper ecosystem. Keep Apache for legacy PHP applications and shared hosting environments where.htaccess support is non-negotiable.

The best web server is the one your team can configure correctly, monitor effectively, and operate without surprises at 3 AM. For most teams in 2025, that's Caddy.

A

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

Enjoyed this article?

Get more like this in your inbox. No spam, unsubscribe anytime.

Comments

Loading comments...

Leave a comment

Stay in the loop

New articles delivered to your inbox. No spam.