Leaving Apache for nginx or Caddy? I measured them: under load, nginx serves 3 times more PHP requests

Table of contents (8 sections)

I host my clients' sites and my personal projects on a dedicated server running Debian (a Linux system). A web server is the software that receives requests from browsers, the requests, and sends back the pages. Mine is Apache. On November 11, 2026, the Apache project released version 2.4.71. It fixes a critical flaw in one of its extensions, the HTTP/2 module. Should you leave Apache for nginx or Caddy, two other widely used web servers?

To decide, I read the security advisory and looked at what my server does when a flaw lands. Then I measured four web servers (Apache, nginx, Caddy and OpenLiteSpeed) on the same PHP, the language behind many sites, WordPress included. My answer: the flaw is not a reason to migrate, but throughput might be one. Throughput is the number of requests a web server handles per second.

The flaw in plain words: what it allows, and who is affected

The flaw is numbered CVE-2026-119618. A CVE is the public identifier of a known flaw. This one hits mod_http2, the Apache module that speaks HTTP/2, the modern version of the web protocol. Two web server tasks running at the same time share a memory area without waiting for each other, and it gets corrupted. An attacker could use this to run their own commands on the machine. That is remote code execution. The Apache advisory rates the flaw "critical".

The affected versions run from 2.4.55 to 2.4.70. The fix ships in 2.4.71, released on November 11, 2026. The flaw was first reported on October 26, then again on November 3. So the fix took 16 days to ship after the first report. Red Hat scores its severity at 9.8 out of 10. According to Wiz, a security vendor, no exploit code was public and no attack was reported when its post came out.

The credited discoverers are Anthropic's automated OSS Scanner tool and two Google Cloud researchers. I should say that I use Anthropic's Claude assistant, so I have a link with one of them.

Checking if you are exposed, in three commands

On Debian, three commands are enough:

apache2 -v
apt policy apache2
apache2ctl -M | grep http2

The first prints the version. The second compares the installed version with the one in the repository, Debian's package server. The third tells you whether the HTTP/2 module is loaded. The flaw targets that module: without it, you are not concerned. On Debian 13 (trixie), the fixed version is written 2.4.71-1~deb13u1. The start is the Apache number, the end is the Debian package revision.

Here Debian took the version published by the Apache project, 2.4.71, so the number shown by apache2 -v is enough. That is not true everywhere. A distribution that fixes a flaw without changing the number will mislead you if you only compare with the Apache advisory. Compare with your distribution's package. The Debian security tracker gives the fixed version for each CVE.

While waiting for the fix, Wiz suggests removing HTTP/2 with one line of configuration, the Protocols http/1.1 directive. I did not test this workaround.

What really decides: the time between the advisory and your patched server

My first question was not "which server is the safest?" but "how long was I exposed?". Three delays follow one another between the discovery of the flaw and my patched server:

  1. Apache writes and publishes the fix: 16 days after the first report, as seen above.
  2. Debian pulls it into its security repository: its advisory DSA-6620-1 is dated November 12 at 09:54 UTC, about one day after Apache.
  3. My server installs it.

The first two delays do not depend on me. The third one does: my dedicated server installs Debian's security updates on its own once a day, with unattended-upgrades. This time its run had not happened yet when I learned about the flaw, so I installed the fix by hand. Since then I have set the two systemd timers, systemd being Debian's service manager, to once an hour: apt-daily checks, apt-daily-upgrade installs. Two gaps remain: no automatic reboot after a kernel update, and no email if an update fails.

Changing servers shortens none of these three delays. nginx and Caddy also publish security advisories: nginx list, Caddy list. An Apache patched as soon as Debian publishes beats an nginx you forget to update for a month. So security does not separate the servers. That leaves throughput, and that one can be measured.

The test bench: four servers, the same pages

I wanted numbers on my own setup, not someone else's. So I built a bench. Here are its settings:

  • Four web servers, in their November 13, 2026 versions: Apache 2.4.71 (patched), nginx 1.31.8 and Caddy 2.11.9. The fourth is OpenLiteSpeed 1.9.4, the free version of LiteSpeed. Each runs in a Docker container, an isolated box that does not touch the rest of my machine.
  • The same PHP-FPM for all: the service that runs pages written in PHP (version 8.3.36), with 32 fixed processes. OpenLiteSpeed is measured in two ways. With LSAPI, LiteSpeed's protocol for talking to PHP, it uses the PHP 8.2 shipped in its image: that is its normal mode. With FPM, I plug it into the same PHP-FPM as the others. I call this second mode "OLS + FPM".
  • Two pages: a static HTML file of 20,329 bytes, and a PHP script that writes "ok". PHP does almost nothing, so I mostly measure the web server.
  • The load tool is wrk, run on the same machine: a Windows PC that runs Linux through WSL2, with 24 cores (the processor's compute units). It opens a fixed number of simultaneous connections, meaning clients that send their requests at the same time, for five seconds. I report the median of three runs.
  • A minimal configuration written by hand for Apache, nginx and Caddy: HTTP only, no HTTPS (the encrypted connection), no HTTP/2 and no access log (the list of requested pages). Two settings depart from the defaults: Apache accepts 1,000 requests per connection instead of 100, and nginx 1,024 connections per worker process. OpenLiteSpeed keeps the original configuration of its image, with a few cuts: access log, QUIC (HTTP/3), compression and reading of .htaccess. Only the dedicated test reads that file. I give it 32 PHP processes.

The typical command, here for 64 connections on the PHP page:

wrk -t4 -c64 -d5s --latency http://web/index.php

I varied -c (the simultaneous connections) from 1 to 1,024 on each page. This is the "sweep".

Two measures come up. Requests per second give the throughput. The p99 is the delay that only 1% of requests exceed: it is the common worst case.

Limits: a single machine, a trivial PHP page, no HTTPS, and the load generator shares its cores with the servers. These numbers give orders of magnitude, not a verdict.

Two episodes of the measurement are worth telling. A first PHP run gave nginx half the score of all the later ones, so I discarded it. I redid it at 64 connections, alternating the servers, four times in a row, so that the machine's state weighs equally on each. This is the "alternating series". Separately, two settings I tried along the way lowered throughput. For Apache, it is connection reuse towards PHP-FPM. For nginx, it is persistent connections towards PHP-FPM: 5.0 thousand requests per second at 64 connections, instead of 36.1. I did not look into why.

The results: OpenLiteSpeed and nginx ahead, Apache capped in PHP

At 64 simultaneous connections (tables below), nginx serves the static page about 2.8 times faster than Apache. For the PHP page, it is about 3 times faster. Still in PHP, the gap falls to 1.5 times at 16 connections (17.4 against 11.8 thousand requests per second). The alternating series confirms the PHP gap: 35.9 against 11.0 thousand requests per second for nginx and Apache, that is 3.3 times. The curves show what happens as the load rises.

Throughput on a PHP page by number of simultaneous connections, for nginx, Apache, Caddy and OpenLiteSpeed020406080116642561024Simultaneous connectionsthousand requests / secondOpenLiteSpeednginxOLS + FPMCaddyApache
PHP page: Apache stays flat from 16 connections, nginx and Caddy keep climbing. OpenLiteSpeed, in amber, levels off much higher: solid line with its built-in PHP, dashed (OLS + FPM) with the same PHP-FPM as the other servers, where it drops at 1,024 connections. Each step on the axis multiplies the number of connections by 4: from 1 to 16 there are two steps. nginx in green, Apache in red, Caddy in blue.

In PHP, Apache hits a ceiling. Its throughput stops growing from 16 connections, at about 11.8 thousand requests per second, and stays there up to 1,024. nginx and Caddy keep climbing. At 1,024 connections, Apache's p99 climbs to about 1.1 seconds, against 28 ms for nginx and 45 ms for Caddy.

Apache also loses connections there. wrk counts cut or timed-out connections there, at 4.8% of the number of requests served. nginx and Caddy count 7 errors in total on the same run. I did not look for the cause.

OpenLiteSpeed passes nginx in PHP. With its built-in PHP, it holds 68.3 to 72.8 thousand requests per second between 64 and 1,024 connections. Its p99 is about 21 ms at 1,024. The alternating series gives 63.8 thousand at 64 connections, against 35.9 for nginx. With the same PHP-FPM as the others, it does 51.3 thousand, but it is less steady. It counts a few dozen errors over about 550,000 requests. At 1,024 connections, one run in three served nothing. Two caveats: its normal mode uses PHP 8.2 and not 8.3, and I did not explain the gap with nginx.

Throughput on a static page by number of simultaneous connections, for nginx, Apache, Caddy and OpenLiteSpeed050100150200116642561024Simultaneous connectionsthousand requests / secondOpenLiteSpeednginxCaddyApache
Static page: each server's throughput by number of simultaneous connections. nginx peaks at 64 connections then drops, Caddy keeps climbing, OpenLiteSpeed tops out lower than nginx but holds the load better. Each step on the axis multiplies the number of connections by 4: from 1 to 16 there are two steps. nginx in green, Apache in red, Caddy in blue, OpenLiteSpeed in amber.

On the static page, the story changes. nginx peaks at 64 connections (179.0 thousand requests per second) then drops to 90.4 thousand at 1,024. Caddy climbs all the way and then passes Apache (80.1 against 59.5 thousand). For Apache, wrk counts cut connections at 4.3% of the number of requests served from 256 connections.

OpenLiteSpeed matches nginx at 16 connections (103.8 against 100.8 thousand) and peaks at 122.6 thousand at 64. It passes nginx from 256 on (132.2 against 119.5). Its runs at 1,024 connections range from 91 to 145 thousand: keep the order of magnitude, not the figure. The page does not touch PHP, so I pooled the six runs of both modes.

To go back over the exact numbers, in thousands of requests per second:

PHP, simultaneous connections:116642561,024
nginx2.517.433.440.542.7
Apache1.311.811.411.711.8
Caddy1.110.922.827.129.5
OpenLiteSpeed4.016.572.870.468.3
OLS + FPM3.633.153.859.836.1
Static, simultaneous connections:116642561,024
nginx8.9100.8179.0119.590.4
Apache5.750.865.077.359.5
Caddy3.931.259.065.880.1
OpenLiteSpeed7.8103.8122.6132.2139.2

Two other measures complete the picture. The first is about .htaccess, a small configuration file you drop in a site folder and that Apache rereads on every request. WordPress writes one for its addresses. With AllowOverride All, the setting that allows these files, and a .htaccess of this kind, Apache loses about 6.5% of throughput. This is measured on the static page, at 64 connections. I measured it in a separate test (ten seconds per run), with and without the file. OpenLiteSpeed reads this file by default. Its cost is not stable: 5.7% more in static, 6.9% less in PHP. The differences go in opposite directions, so they are within noise. The second measure is memory:

Server memory (MiB, about one million bytes)At restAfter the load
Apache6.825.9
nginx19.923.6
Caddy14.750.2
OLS + FPM13.050.9

nginx starts one process per visible core, so 24 here (counted inside the container), which probably explains its memory at rest. Keep the order of magnitude instead: without counting PHP, all four stay under 51 MiB. In LSAPI mode, OpenLiteSpeed also starts its PHP processes inside its container: 35.8 MiB at rest, 70.0 after the load. This is not comparable, since PHP runs separately for the others.

My bench shows what web servers are worth when PHP does almost nothing. I did not measure a PHP page that really works. My take is that PHP would then become the limiting factor, and the gap would narrow. A Tideways test, published in September 2025, also finds a much smaller gap between nginx and Caddy than mine. On an HTML page, it gets 7,023 requests per second for nginx with PHP-FPM. FrankenPHP, a server built on Caddy, gets 6,934. Benches diverge, hence the value of measuring at home.

The table to choose: Apache, nginx, Caddy, LiteSpeed

Cloudflare comes first in the W3Techs ranking, a site that tracks the technologies used by websites, with 33.8% of sites. Yet it is not a server you install at home: it is a delivery network placed in front of yours. The real alternatives to Apache are therefore nginx, Caddy and LiteSpeed. I measured its free version, OpenLiteSpeed. I did not test LiteSpeed Enterprise, the paid version.

ApachenginxCaddyOpenLiteSpeed
Share of sites (W3Techs, November 13, 2026)20.9%30.0%1.2%13.4% (LiteSpeed, all versions)
.htaccess filesYes, but the docs advise against themNoNo, I found no supportYes, tested: WordPress's rewrite rule passes
PHP throughput at 64 connections (thousand req/s)11.433.422.872.8 (built-in PHP) or 53.8 (with PHP-FPM)
Static throughput at 64 connections (thousand req/s)65.0179.059.0122.6
p99 in PHP at 1,024 connections1.1 s28 ms45 ms21 ms (built-in PHP)
Migration from ApacheNoneRewrite every ruleRewrite every ruleLow: WordPress's rewrite rule passes as is. My other rules remain to be tested
My take: choose it ifYour sites ship their own .htaccess (CMS, clients)You control the config and have a lot of trafficYou want automatic HTTPS and a short configYou want to keep your .htaccess and the best PHP throughput of my bench. Try it first on a test machine

To me, Caddy's strong point is not throughput, where it stays behind nginx. It gets and renews HTTPS certificates on its own, from free authorities like Let's Encrypt or ZeroSSL. A certificate is the file that proves a site's identity and encrypts the connection.

The migration cost depends on your sites. In my case, some are CMSs (content management tools like WordPress) that ship their own .htaccess. On nginx, each rewrite rule (which changes the requested address, for example to index.php) is rewritten by hand. The official nginx blog says it: nginx does not handle per-directory files like Apache. I found neither an official converter nor an independent test of third-party converters.

Apache, for its part, recommends putting all the configuration in the main file when you have access to it. That is the same advice as my measures on Apache: .htaccess costs.

My take: the flaw does not justify migrating, throughput maybe

My take: the flaw is not a reason to migrate. The patching delay weighs more than the choice of server, and nginx and Caddy also publish advisories. Throughput, on the other hand, can be one, if your load is mostly light PHP or high-traffic static. Otherwise, Apache stays fine at the scale of my sites: a dedicated server, client sites, personal projects.

If throughput mattered, I would try OpenLiteSpeed first. It is the only candidate on my bench that keeps .htaccess files and passes nginx in PHP. I would do it on a test machine, with my real sites, before touching client sites.

What I miss is elsewhere: an update report by email, and a scheduled reboot for the kernel.

Conclusion

I set out to find a safer server and found another variable: the delay between the security advisory and the fix installed on your machine. The choice of web server weighs less.

Still, the curves deserve a look: they show where each server gives out. In PHP, OpenLiteSpeed passes nginx at 64 and 256 connections in both modes, and up to 1,024 with its built-in PHP. nginx stays ahead of Apache and Caddy at every load level, even when its static curve drops after 64 connections. Against Apache, nginx's gap is 1.5 times in PHP at 16 connections, and 3.6 times at 1,024.

Sources

Documentation consulted on 13 November 2026:

In the Developer's Great Book

A free book where each idea is told once, in its place, from silicon to human judgment.

Comments (0)