← Back to Blog

WordPress on a VPS: Nginx, OpenLiteSpeed, Caddy, FrankenPHP and Apache Load-Tested, Plus Redis and OPcache

Published · by RS Computers

WordPress Benchmarks Web servers

On a small WordPress VPS, the page cache decides your speed far more than the web server does. We load-tested WordPress 7.1.3 on one 2 vCPU, 2 GB server with five stacks: Nginx, Apache, Caddy, FrankenPHP and OpenLiteSpeed, all running PHP 8.4. Without a cache, all five served 25 to 28 pages per second, a tie, because PHP used every CPU cycle. With each stack's own page cache, Apache served about 920 pages per second, Caddy, FrankenPHP and Nginx 2,200 to 2,800, and OpenLiteSpeed with LSCache 30,000. Most of that gap came from one detail: whether the cached page was stored already compressed.

Key facts, checked on 13 October 2026:

The results at a glance

Completed requests per second at 50 simultaneous connections, median of three 15-second runs. "Search" is a request that every cache here passes to PHP, like a logged-in visitor or a cart. RAM is the peak memory of all processes on the server, MariaDB included.

Stack (versions tested)Page cacheHomepage, no cacheHomepage, cachedSearchPeak RAM
Nginx 1.26.3 + PHP-FPM 8.4.26fastcgi_cache (built in)24.72,81125.5253 MB
Apache 2.4.68 (event MPM) + PHP-FPM 8.4.26WP Super Cache 3.1.4 via mod_rewrite26.691625.1292 MB
Caddy 2.11.7 + PHP-FPM 8.4.26WP Super Cache files served by Caddy28.22,21324.7290 MB
FrankenPHP 1.13.0 (Caddy 2.11.7, PHP 8.4.26)WP Super Cache files served by Caddy27.82,26725.3300 MB
OpenLiteSpeed 1.9.3 + lsphp 8.4.26LSCache plugin 7.9.127.630,43424.5259 MB

Storing the cached pages gzipped lifted FrankenPHP from 2,267 to 10,202 cached requests per second, with no other change.

How we tested

This was one shared lab host, so read every number as relative, stack against stack. The server under test was an Incus container with 2 vCPU and 2 GiB of RAM, the size of our VPS Micro, on an Amsterdam lab machine (8 cores, 15 GB) that other test workloads used at the same time. The load came from a second 2 vCPU container on the same machine, so no internet sat in between.

Each stack started from the same saved container: Debian 13, MariaDB 11.8.6, WordPress 7.1.3 with the Twenty Twenty-Five theme (1.5), and sample content made with WP-CLI 2.12.0:

# as root, in /var/www/wp (body.html holds about 600 words of sample text)
sudo -u www-data wp post generate --count=200 --post_content < /tmp/body.html
sudo -u www-data wp term generate category --count=10
sudo -u www-data wp rewrite structure '/%postname%/'
sudo -u www-data wp db size --human-readable

Generating the 200 posts took about 2 seconds, and the database stayed under 1 MB. The homepage lists 10 posts and weighs 117 KB of HTML. Every stack got 10 PHP workers (pm.max_children = 10 for PHP-FPM, OpenLiteSpeed's default of 10 LSAPI children, num_threads 10 for FrankenPHP) and kept OPcache on. OpenLiteSpeed and FrankenPHP bring their own PHP, so we stopped PHP-FPM on those two.

Three requests, three concurrency levels

We confirmed that the search skipped every cache: Nginx answered X-Cache: BYPASS, OpenLiteSpeed sent x-litespeed-cache-control: no-cache, and with PHP stopped, Apache and Caddy still served the cached homepage but failed the search. The load tool was oha 1.16.0 with 10, 50 and 100 open connections, 15 seconds a run, three runs each. It asks for compressed pages as a browser does, so every stack compressed its answers.

# as any user, on the load-generator machine
oha --no-tui -z 15s -c 50 http://wp.test/

On Nginx without a cache it printed Requests/sec: 29.9958 next to [200] 400 responses and [50] aborted due to deadline. That Requests/sec line includes the 50 requests cut off when time ran out, so we divided completed answers by the duration instead: 400 in 15 seconds is 26.7 per second. Check which of the two any benchmark you read reports.

For memory and CPU we read the server container's own counters, which cover every process inside it, MariaDB included:

# as root, inside the server container
awk '/^(anon|file|shmem) /{printf "%s %d MB\n", $1, $2/1048576}' /sys/fs/cgroup/memory.stat
awk '/^usage_usec/{print $2}' /sys/fs/cgroup/cpu.stat
free -m

On Nginx after the tests this showed anon 250 MB (memory used by programs), file 79 MB (disk cache that Linux gives back when needed) and shmem 27 MB. We sampled anon every second and kept the peak, and read the CPU counter before and after each run. On a normal VPS, free -m and top do the same job; our VPS benchmark guide covers the hardware side.

To measure the noise, we ran the uncached Nginx test again at the very end: 27.3, 28.3 and 25.4 pages per second at 10, 50 and 100 connections, against 27.5, 24.7 and 22.0 at the start. Uncached differences under about 15% are noise, not a winner.

Without a cache, PHP is the bottleneck on every stack

Homepage, no cache (requests/s, p50/p95)10 connections50 connections100 connections
Nginx27.5, 357/416 ms24.7, 1.94/2.26 s22.0, 3.91/5.40 s
Apache26.9, 362/410 ms26.6, 1.84/1.96 s23.8, 3.67/4.93 s
Caddy27.5, 356/395 ms28.2, 1.73/1.89 s24.0, 3.61/4.72 s
FrankenPHP27.9, 352/399 ms27.8, 1.74/1.90 s25.3, 3.48/4.43 s
OpenLiteSpeed27.8, 322/377 ms27.6, 1.27/1.89 s26.0, 2.45/5.10 s

p50 is the median wait; p95 is the wait that 95% of requests beat, which is what your unluckiest visitors get. Both vCPUs ran at 100% in every uncached run, which works out at about 73 ms of CPU per page. More visitors did not mean more pages, only longer waits. At 10 connections pages came back in about 0.4 seconds, inside the 0.8 seconds that web.dev calls a good time to first byte. At 100 connections the median passed 3 seconds on four stacks.

OpenLiteSpeed had a lower median at 50 and 100 connections but the same p95 and throughput, so it served requests in a different order, not faster. Apache dropped 1 to 3 connections in some runs ("connection closed before message completed"). Its event MPM documentation gives the likely cause: when all workers are busy, it closes idle keep-alive connections early so the client can reconnect. Browsers do that on their own.

With a page cache, compression decides the winner

A full-page cache saves the finished HTML and hands it to the next visitor without running PHP. We used each stack's native option. Nginx has fastcgi_cache built in, and OpenLiteSpeed has a cache module driven by the free LSCache plugin. Apache and standard Caddy have no WordPress-aware page cache (Caddy's cache-handler needs a custom build), so they served the static files that WP Super Cache writes, without PHP.

Homepage, cached (requests/s, p95)10 connections50 connections100 connections
Apache + WP Super Cache982, 32 ms916, 198 ms965, 301 ms
Caddy + WP Super Cache2,248, 10 ms2,213, 39 ms2,150, 74 ms
FrankenPHP + WP Super Cache2,307, 10 ms2,267, 39 ms2,211, 74 ms
Nginx fastcgi_cache2,756, 5 ms2,811, 20 ms2,750, 41 ms
FrankenPHP, gzipped cache files9,887, 3 ms10,202, 13 ms9,881, 26 ms
OpenLiteSpeed + LSCache28,330, 1 ms30,434, 2 ms30,712, 4 ms

Medians were 0 to 47 ms everywhere. Nginx, Apache, Caddy and FrankenPHP kept the cached page as plain 117 KB HTML and compressed it again for every visitor, with both vCPUs busy doing little else. Two extra runs proved it. The same Nginx cache, asked for uncompressed pages, delivered 27,003 requests per second. And when WP Super Cache stored a 13.5 KB gzip copy of each page and FrankenPHP sent that file directly, throughput rose 4.5 times. OpenLiteSpeed did not pay that cost on each hit, which explains most of its lead. At 30,000 requests per second our load generator was at half its CPU, so read that figure as "at least". Apache was slowest and dropped about 0.1% to 0.3% of connections at 50 and 100 connections. Raising MaxKeepAliveRequests from 100 to 1000 did not stop that, and we did not profile it further.

Keep these numbers in proportion. Apache's 916 cached pages per second is 3.3 million page views an hour, and a 1 Gb/s port carries only about 9,200 pages per second at 13.5 KB each, so beyond that the network is the limit. For a real site, the question that matters is how many requests miss the cache.

Requests that skip the cache: search, carts and logged-in users

Search with cache on (requests/s, p50/p95)10 connections50 connections100 connections
Nginx23.9, 401/492 ms25.5, 1.93/2.03 s22.3, 3.89/5.34 s
Apache22.5, 424/530 ms25.1, 1.92/2.08 s21.9, 3.95/5.29 s
Caddy24.9, 388/458 ms24.7, 1.98/2.13 s21.8, 3.88/5.26 s
FrankenPHP25.0, 391/436 ms25.3, 1.91/2.08 s22.3, 3.82/5.32 s
OpenLiteSpeed25.1, 361/424 ms24.5, 1.26/2.27 s22.3, 2.53/6.73 s

Logged-in users, carts, checkouts, search and wp-admin all skip the page cache, and here the five stacks tie again at about 25 pages per second on 2 vCPUs. A search ran 33 database queries against 29 for the homepage, so it was a little slower. If most of your traffic looks like this, only more CPU helps. Our WooCommerce VPS guide goes deeper on shops.

PHP 8.3 vs 8.4, OPcache and Redis object cache

These ran on the Nginx stack, homepage without a page cache, three runs each.

Nginx, homepage, no cache10 connections50 connections100 connectionsPeak RAM
PHP 8.4.26 (Debian)26.9/s27.6/s23.1/s253 MB
PHP 8.3.35 (Sury repository)27.7/s27.9/s25.2/s241 MB
PHP 8.4, OPcache off6.5/s4.0/s1.1/s535 MB
PHP 8.4 + Redis object cache26.6/s26.9/s23.3/s253 MB + 4 MB Redis

PHP 8.3 against 8.4

A tie: we ran the two back to back and the gap stayed inside the noise. Debian 13 has no PHP 8.3, so it came from the Sury repository. Use 8.4, Debian's own package, which gets security fixes a year longer.

OPcache on and off

OPcache keeps PHP code compiled in memory. Without it, the server made 76% fewer pages at 10 connections and needed twice the memory; at 100 connections most requests did not finish within 15 seconds. Debian turns it on by default, but hand-made configs sometimes switch it off. Check yours:

# as root
php-fpm8.4 -i | grep -E "opcache.enable =>|opcache.memory_consumption"

You want opcache.enable => On => On; ours also showed 128 MB of OPcache memory.

Redis object cache

An object cache keeps the results of database queries in memory. We used the Redis Object Cache plugin 3.0.0 with Debian's Redis 8.0.2:

# as root
apt-get install -y redis-server php8.4-redis
systemctl restart php8.4-fpm
cd /var/www/wp
sudo -u www-data wp plugin install redis-cache --activate
sudo -u www-data wp redis enable
sudo -u www-data wp redis status

The status showed Status: Connected, Client: PhpRedis (v6.2.0) and Redis Version: 8.0.2. Queries per homepage fell from 29 to 7, yet throughput did not move: the database was not where the time went. Redis tends to pay off when queries are slow or many, as on WooCommerce shops, sites with long plugin lists and sites with many logged-in users. See our database server guide.

The cache setups we used

Only the parts that differ between stacks are here. For a full install with a domain and a control panel, follow our WordPress hosting guide; for HTTPS, the reverse proxy and SSL guide. Our tests used plain HTTP on a lab hostname.

Nginx: fastcgi_cache

# as root: this part goes in /etc/nginx/conf.d/wp-cache.conf
fastcgi_cache_path /var/cache/nginx/wp levels=1:2 keys_zone=WP:32m inactive=60m max_size=256m;
map $http_cookie $wp_skip_cookie {
    default 0;
    ~*(wordpress_logged_in|comment_author|wp-postpass|woocommerce_items_in_cart) 1;
}

# in your site's server block, before the locations
set $skip_cache $wp_skip_cookie;
if ($request_method = POST) { set $skip_cache 1; }
if ($query_string != "") { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/wp-login.php|/xmlrpc.php|/wp-json/|/feed/|sitemap") { set $skip_cache 1; }

# inside location ~ \.php$, after fastcgi_pass
fastcgi_cache WP;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
fastcgi_cache_lock on;
fastcgi_cache_use_stale error timeout updating http_500 http_503;
add_header X-Cache $upstream_cache_status;

Debian 13 has no /var/cache/nginx folder, so create the cache folder first. Without it, nginx -t stops with mkdir() "/var/cache/nginx/wp" failed.

# as root
mkdir -p /var/cache/nginx/wp
nginx -t && systemctl reload nginx
for u in / / "/?s=lorem"; do curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" http://wp.test$u | grep -i x-cache; done

Expected: test is successful, then X-Cache: MISS, X-Cache: HIT and X-Cache: BYPASS for the search. A request with a wordpress_logged_in cookie also shows BYPASS.

Apache, Caddy and FrankenPHP: WP Super Cache

# as root, in /var/www/wp
sudo -u www-data wp plugin install wp-super-cache --activate
sudo -u www-data wp eval '
wp_cache_enable();
wp_cache_setting( "wp_cache_mod_rewrite", 1 );
wp_cache_setting( "wp_cache_no_cache_for_get", 1 );
wp_cache_setting( "wp_cache_not_logged_in", 2 );
var_dump( wpsc_update_htaccess() );'

This turns on "expert" delivery, where the web server sends the saved file, and skips URLs with query strings. On Apache it printed bool(true) and wrote a # BEGIN WPSuperCache block into .htaccess. Caddy and FrankenPHP ignore .htaccess, so there we dropped the var_dump line and closed the quote after the line above it. A page served from the cache ends with the HTML comment <!-- super cache -->; in one of our runs the first requests after the change still came from PHP, and a few seconds later they were cached. Caddy then needs a rule to find the files (we also added the caddy user to the www-data group so it can reach the PHP-FPM socket):

# as root: /etc/caddy/Caddyfile
:80 {
	root * /var/www/wp
	encode gzip

	# WP Super Cache: serve the saved HTML file without PHP
	@supercache {
		method GET
		expression {query} == ""
		not header_regexp Cookie "comment_author|wordpress_logged_in|wp-postpass"
		file /wp-content/cache/supercache/{host}{path}/index.html
	}
	rewrite @supercache {file_match.relative}

	php_fastcgi unix//run/php/php8.4-fpm.sock
	file_server
}

With PHP-FPM stopped, the homepage still answered 200, while search and a request carrying a wordpress_logged_in cookie failed with 502. On HTTPS, WP Super Cache names the file index-https.html, as the .htaccess rules it writes show, so change the matcher to that name. For the 4.5 times faster variant we ran wp eval 'wp_cache_setting( "cache_compression", 1 );' and served the match from a handle @supercache block with the same rewrite plus file_server { precompressed gzip }, with PHP in a second handle block.

FrankenPHP with PHP 8.4

FrankenPHP is Caddy with PHP built in. Its standalone binary carries PHP 8.5, so for a fair test we used the project's Debian packages with PHP 8.4:

# as root
VERSION=84
mkdir -p /etc/apt/keyrings
curl -fsSL https://pkg.henderkes.com/api/packages/${VERSION}/debian/repository.key -o /etc/apt/keyrings/static-php${VERSION}.asc
echo "deb [signed-by=/etc/apt/keyrings/static-php${VERSION}.asc] https://pkg.henderkes.com/api/packages/${VERSION}/debian php-zts main" | tee /etc/apt/sources.list.d/static-php${VERSION}.list
apt-get update
apt-get install -y frankenphp
apt-get install -y php-zts-mysqli php-zts-mysqlnd php-zts-gd php-zts-intl php-zts-zip
frankenphp version

Expected: FrankenPHP v1.13.0 PHP 8.4 Caddy v2.11.7. The service runs as a frankenphp user that cannot write to a WordPress folder owned by www-data, so we ran it as www-data. That user also needs a writable home for Caddy's data; without one, our log filled with "permission denied" storage errors:

# as root
mkdir -p /etc/systemd/system/frankenphp.service.d
cat > /etc/systemd/system/frankenphp.service.d/override.conf <<'EOF'
[Service]
User=www-data
Group=www-data
Environment=HOME=/var/lib/frankenphp
EOF
chown -R www-data:www-data /var/lib/frankenphp
systemctl daemon-reload
systemctl restart frankenphp

In /etc/frankenphp/Caddyfile we set num_threads 10 in the global frankenphp block and reused the Caddy site block with php_server in place of the last two lines. WordPress ran in classic mode, not worker mode.

OpenLiteSpeed with LSCache

# as root
apt-get install -y wget
wget -O - https://repo.litespeed.sh | bash
apt-get install -y openlitespeed
apt-get install -y lsphp84 lsphp84-common lsphp84-mysql lsphp84-opcache lsphp84-curl lsphp84-intl
/usr/local/lsws/bin/lshttpd -v

It printed LiteSpeed/1.9.3 Open (released 29 September 2026, per the release log). In /usr/local/lsws/conf/httpd_config.conf we pointed the PHP handler from lsphp83 to lsphp84/bin/lsphp, set user and group to www-data, and added a virtual host for /var/www/wp with autoLoadHtaccess 1 and a listener on port 80. One trap: after the user change every page returned 503 and the log said Failed to lock pid file for [/tmp/lshttpd/lsphp.sock]: Permission denied. Deleting /tmp/lshttpd and restarting fixed it. Then the cache:

# as root, in /var/www/wp
sudo -u www-data wp plugin install litespeed-cache --activate
/usr/local/lsws/bin/lswsctrl restart
curl -s -o /dev/null -D - http://wp.test/ | grep -i x-litespeed-cache

The first request shows x-litespeed-cache: miss, the next ones hit. On Apache, the only stack-specific step was a2enmod proxy_fcgi setenvif rewrite and a2enconf php8.4-fpm, after which a2query -M printed event.

Which stack should you pick?

You arePickWhy, from our numbers
A beginner who wants a control panelWhatever your panel installs: Nginx on CloudPanel, Nginx with Apache on HestiaCP, OpenLiteSpeed on CyberPanelUncached speed was the same on all five; just switch on the panel's page cache. Compare panels in our free control panel test.
After top cached speed on a small serverOpenLiteSpeed + LSCache30,434 cached requests per second from one plugin. The trade-off is a config format of its own and a few setup traps.
After the simplest hand-written configCaddy with PHP-FPM, or FrankenPHPA short Caddyfile and automatic HTTPS. Gzip copies in WP Super Cache gave FrankenPHP 10,202 cached requests per second.
Someone who wants the most documentationNginx + fastcgi_cache2,811 cached requests per second with no plugin and the lowest peak RAM (253 MB).
Running a site built on .htaccess rulesApache, event MPM, PHP-FPMYour plugins' rules keep working. Slowest cached (916 per second), still plenty for most sites.
Running a shop or membership siteAny of them, with more vCPURequests that skip the cache ran at about 25 per second on every stack.

What size WordPress VPS do these numbers point to?

Two measurements decide it. An uncached page cost about 73 ms of CPU, so 2 vCPUs made 25 to 28 such pages per second at full load. And the whole server, 10 PHP workers and MariaDB included, peaked at 253 to 300 MB of process memory. Only the VPS Micro row below was measured; the others are our estimates, scaled by vCPU count because PHP work grows with cores. A heavier theme or more plugins will lower all of them.

PlanUncached pages per secondGood for
VPS Nano (1 shared vCPU, 1 GB)about 13 (estimate)One blog or company site with a page cache and about 5 PHP workers.
VPS Micro (2 shared vCPU, 2 GB)25 to 28 (measured)A busy blog, a few small sites or a small shop.
VPS Mini (4 shared vCPU, 4 GB)about 50 (estimate)Several sites behind a panel, or a shop with steady logged-in traffic plus Redis.
VDS Small (4 dedicated vCPU, 8 GB)about 50, steadier (estimate)Shops and membership sites where most requests skip the cache; dedicated vCPUs are not shared with other customers.
VDS Medium and Large (8 or 16 dedicated vCPU)about 100 and 200 (estimate)Large WooCommerce stores, multisite networks and agencies.

A simple rule: size for your busiest hour, so that requests missing the cache use about half of the uncached capacity. If your server is slower than this, our guide on why a VPS is slow shows how to check CPU steal and memory pressure, and the VPS vs VDS comparison explains shared and dedicated vCPUs. Moving an existing site? Use our WordPress migration guide.

Frequently asked questions

Which web server is fastest for WordPress on a VPS?

With a full-page cache, OpenLiteSpeed with LSCache was fastest in our lab at 30,434 cached requests per second on 2 vCPUs, against 2,213 to 2,811 for Caddy, FrankenPHP and Nginx and 916 for Apache. Without a cache, all five tied at 25 to 28 pages per second.

Is OpenLiteSpeed faster than Nginx for WordPress?

For cached pages, yes in our test, mostly because Nginx compressed each cached page again for every visitor. For uncached pages, search and logged-in users there was no real difference. Keeping cached pages gzipped narrows the gap: it took FrankenPHP from 2,267 to 10,202 requests per second.

How much RAM does a WordPress VPS need?

In our lab, the whole server with 10 PHP workers and MariaDB peaked at 253 to 300 MB of process memory. 1 GB is enough for one site with fewer PHP workers; 2 GB leaves room for several sites, Redis and updates.

Does Redis object cache make WordPress faster?

Not always. On our simple blog it cut database queries per page from 29 to 7, but pages per second stayed the same. It helps when the database is the bottleneck, as on WooCommerce shops and sites with many plugins or logged-in users.

Should I use PHP 8.3 or PHP 8.4 for WordPress?

PHP 8.4. It was as fast as 8.3 in our back-to-back test, Debian 13 ships it, and it gets security fixes until 31 December 2028, a year longer than PHP 8.3.

How many visitors can a 2 vCPU VPS handle with WordPress?

From a page cache, 900 to over 30,000 pages per second in our test, depending on the stack. Pages that skip the cache, about 25 per second. So the real limit depends on how much of your traffic is logged in, in a cart or searching.

Try your own site on a server like ours

Turn on a page cache, keep OPcache on, and measure the requests that miss the cache, because that number decides your plan. Our KVM servers run in Amsterdam, Dublin and Prishtina with NVMe storage, unmetered traffic and their own IPv4 and IPv6 addresses. See the WordPress VPS page or every plan on the plans page, where availability by city is shown, or message us on Telegram with a rough idea of how much of your traffic is logged in, and we will help you pick a size.

← All articles

Chat on Telegram