Nginx, Apache, or Caddy? Choosing a Web Server, with Benchmarks and the Settings That Matter

We benchmarked nginx 1.30, Apache 2.4, and Caddy 2.11 on the same two CPU cores, serving static files and proxying to a Node.js backend. Nginx was fastest, but the most useful finding was elsewhere: with default proxy settings, both nginx and Caddy started returning 502 errors under load. When to choose each server, and the settings that are most often missed.

Published 11 min read
A server rack patch panel with rows of red network cables

Every web application sits behind a web server, and most teams pick one out of habit: nginx because the last project used it, Apache because the hosting came with it. Both habits are usually fine. What causes trouble in production is rarely the choice of server, but a default setting nobody looked at. To show both sides, we ran nginx, Apache, and Caddy through the same tests: serving static files and acting as a reverse proxy in front of an application. The throughput results were roughly what you would expect. The proxy results were not.

What the Web Server Does Today

In a modern setup the web server rarely runs the application itself. It sits in front of it and handles the work that every application needs: terminating HTTPS, serving images, CSS, and JavaScript directly from disk, compressing responses, enforcing upload limits and timeouts, and passing everything else to the application. For Laravel and WordPress that application is PHP-FPM; for Next.js, NestJS, Express, Go, or Django it is an application server listening on a local port. That last role, the reverse proxy, is where most production problems start.

The Candidates

  • Nginx: an event-driven server built to handle many connections with few processes and little memory. It is the most common choice for reverse proxying and static files. The current stable branch is 1.30, released in April 2026; version 1.30.6 from 14 October fixed five security issues.
  • Apache HTTP Server: the oldest of the three and still everywhere, especially on shared hosting. Its main advantage is .htaccess, which lets each site change rewrite rules and settings without touching the main configuration, and which WordPress and many PHP applications rely on. Modern Apache with the event MPM is far more efficient than its reputation from the prefork days.
  • Caddy: a newer server written in Go whose defining feature is automatic HTTPS. Point a domain at the server, write two lines of configuration, and Caddy obtains and renews certificates by itself, with HTTP/3 enabled by default. The configuration format is much shorter than nginx or Apache.
  • LiteSpeed: a commercial server that reads Apache configuration and .htaccess files, used by many shared hosting providers in Indonesia, mainly because of its built-in WordPress cache. Its free edition, OpenLiteSpeed, supports only part of .htaccess.

How We Tested

We ran nginx 1.30.6, Apache 2.4.69 with the event MPM, and Caddy 2.11.7 in Docker, one at a time, each limited to the same two CPU cores. The load generator, oha, ran on three other cores, and a small Node.js backend on a separate core. Each server used its default configuration with access logging switched off, plus the lines needed to serve files and proxy to the backend. Every test used 100 concurrent connections for 15 seconds, after a warm-up, and the table shows the median of three runs. The files were a 2 KB HTML page and a 100 KB JavaScript file.

nginx 1.30.6Apache 2.4.69Caddy 2.11.7
2 KB HTML page, requests per second37,93219,31513,361
2 KB HTML page, 99th percentile latency4.6 ms98.0 ms23.4 ms
100 KB JavaScript file, requests per second32,25614,4318,312
Reverse proxy with connection reuse, requests per second24,07416,94212,920
Memory while idle6.4 MB7.4 MB11.0 MB
Memory during the proxy test7.8 MB31.5 MB37.7 MB

Nginx served about twice as many requests as Apache and three times as many as Caddy, with the lowest latency and a fraction of the memory. Apache's high tail latency comes partly from its defaults, which close a keep-alive connection after 100 requests and start with a limited number of threads; tuning those narrows the gap. Caddy was the slowest here, but 13,000 requests per second on two cores is still more than a billion requests a day. For the vast majority of business websites and applications, all three are fast enough, and the application behind them will be the bottleneck long before the web server is.

The Real Finding: 502 Errors from a Default Setting

The table above shows the proxy results with connection reuse switched on. With the default settings, the picture was very different. Out of the box, nginx talks to the backend using HTTP/1.0 and opens a new TCP connection for every single request. In the first five seconds it managed about 3,500 requests per second, and left 17,323 closed connections waiting in the TIME_WAIT state. Linux keeps a closed outgoing connection in that state for 60 seconds, and only about 28,000 local ports are available by default. Once they ran out, nginx could not open new connections at all. In the following 15-second run, 6,329 of 6,690 requests ended in 502 Bad Gateway.

Caddy does reuse connections by default, but it keeps at most 32 idle connections per backend. With 100 concurrent requests, the rest were opened and closed continuously: 16,001 connections in TIME_WAIT after eight seconds, and in a later run more than half of the requests failed with 502. Raising the limit to 256 brought TIME_WAIT down to 11 and lifted throughput from about 6,800 to 11,800 requests per second. Apache was the only one of the three that reused backend connections well with its defaults.

In numbers: 28,000 ports divided by 60 seconds means trouble starts at roughly 470 new backend connections per second to the same address. A busy API, a flash sale, or a load test can reach that easily, and the symptom, random 502 errors that disappear when traffic drops, is often blamed on the application. The fix is a few lines of configuration:

  • Nginx: define the backend in an upstream block with keepalive 64 (or more), and in the location set proxy_http_version 1.1 and proxy_set_header Connection with an empty value.
  • Caddy: inside reverse_proxy, add a transport http block with keepalive_idle_conns_per_host set to a value above your expected concurrency, such as 256.
  • Apache: connection reuse to HTTP backends is on by default in mod_proxy_http. Check that nothing in the configuration switches it off with disablereuse.

When the backend runs on the same machine and is reached through 127.0.0.1, Linux can reuse ports in TIME_WAIT and the limit is softer. When the backend is another container, another server, or a load balancer, it is not. Either way, a new connection per request also adds a TCP handshake to every call, which is why throughput was already far lower before the first error appeared.

Which One to Choose

  • A VPS or cloud server running Laravel, Next.js, Node.js, Go, or Python: nginx. It is the most documented option for reverse proxying, uses the least memory, and every framework has an nginx example in its deployment guide.
  • WordPress or PHP applications that depend on .htaccess, or shared hosting: Apache, or LiteSpeed if your host offers it. Converting years of .htaccess rules to nginx is work that rarely pays off for a small site.
  • A small team that wants HTTPS without managing certificates, internal tools, or many small sites on one server: Caddy. The short configuration and automatic certificates save more time than the throughput difference costs.
  • Kubernetes: the choice is the ingress or gateway controller, not the web server. Note that the community Ingress NGINX controller was retired in March 2026 and no longer receives security fixes. Nginx itself is unaffected, but clusters still using that controller should move to a supported controller or the Gateway API.

Settings That Are Often Missed

  • Upload limits. Nginx rejects request bodies larger than 1 MB by default with a 413 error, which breaks file and image uploads. Set client_max_body_size to what the application actually needs, and remember PHP has its own upload_max_filesize and post_max_size.
  • Proxy timeouts. Nginx waits 60 seconds for the backend by default. Long exports or reports end in a 504 Gateway Timeout. Raise the timeout only for those routes, or better, move long jobs to a queue.
  • Forwarded headers. The application needs to know the real client IP and that the original request was HTTPS, or it will log the proxy's address and generate http links or redirect loops. Nginx needs proxy_set_header lines for X-Forwarded-For and X-Forwarded-Proto; Caddy sends them automatically. In Laravel, configure the trusted proxies to match.
  • Compression. The default nginx configuration in the official Docker image ships with gzip commented out, and many distribution configs only compress HTML. Enable it for CSS, JavaScript, JSON, and SVG, or serve pre-compressed files. Caddy needs the encode directive, and Apache needs mod_deflate configured.
  • Caching for static files. Files with a hash in their name, such as the assets from Vite or Next.js, can be cached by browsers for a year. Without a Cache-Control header, visitors download them again far more often than needed.
  • Version disclosure. Turn off the version number in responses and error pages with server_tokens off in nginx or ServerTokens Prod in Apache. It does not fix vulnerabilities, but it stops automated scanners from picking your server out by version.
  • Updates. Nginx 1.30.6 alone fixed five CVEs in October 2026. Distribution packages such as Ubuntu's keep older version numbers but receive backported fixes, so update through the package manager and restart the service; the official Docker images need a rebuild.

Before Changing Web Servers

Switching web servers is rarely the fastest way to make a site faster. If pages are slow, the cause is almost always the application, the database, or missing caching. Measure first: if the web server process is not the one using the CPU, a different web server will not help. What does pay off is reviewing the configuration you already have against the list above, and testing it under realistic load before a campaign or launch rather than during one. If you would like a second pair of eyes on your server setup, our team handles this for clients as part of our managed service.

Key takeaways

  • In our test on two CPU cores, nginx served about 38,000 small static requests per second, Apache about 19,000, and Caddy about 13,000. All three are fast enough for most business applications.
  • Nginx used the least memory by far: under 8 MB during the proxy test, against more than 30 MB for Apache and Caddy.
  • With default settings, nginx opens a new backend connection for every request and Caddy keeps only 32 idle connections per backend. Under load both ran out of ports and returned 502 errors.
  • Enable upstream keepalive in nginx and raise keepalive_idle_conns_per_host in Caddy. Apache reuses backend connections by default.
  • Choose nginx for proxying applications on a VPS, Apache or LiteSpeed for .htaccess-based PHP sites, and Caddy when automatic HTTPS and simple configuration matter most.
  • Check upload limits, proxy timeouts, forwarded headers, compression, static caching, and updates. Those settings cause more incidents than the choice of server.
Share this article

Related articles

More articles on software development, AI, cloud, and infrastructure.

Let’s talk

Looking for a software development partner?

Tell us about your project, what you need to build, and the challenges you are facing. We can discuss the technical approach, scope, timeline, and estimated cost.

Start a conversation