↓ Skip to main content
  1. Posts/

WordPress vs. static: I measured TTFB on three identical servers

Table of Contents

The setup
#

Three VPS, all running Debian 12 on 2 vCPU, 4 GB RAM and NVMe storage, with nginx in front of everything and all of them in a single data centre. Each one ran a different stack:

  • Server A: WordPress 6.7, PHP 8.3-FPM, MariaDB 10.11, no cache plugin (yet)
  • Server B: Ghost 5.x on Node.js 20, with SQLite
  • Server C: a Hugo site, with nginx serving /var/www/html/ and nothing else

The three sites look the same and carry the same 20 posts, the same images and the same markup. Cloudflare handles DNS for the public URL but nothing more, so there’s no proxy and no CDN caching. I’m measuring the origin, not the edge.

For load I used k6 with 100 virtual users for 60 seconds and a 10s ramp-up. Each server got 3 runs, and the numbers below come from the median run.

Out of the box
#

                   TTFB p50    TTFB p95    RPS     errors
WordPress (bare)     412 ms     1.38 s      238     0
Ghost                 94 ms     198 ms      920     0
Hugo (static)         11 ms      27 ms    14,200    0

At the median, WordPress is already 37x slower than Hugo, and at p95 the gap is around 50x. WordPress pins the CPU within seconds, and MariaDB is where most of that time goes.

WordPress with caching
#

Nobody runs bare WordPress in production, so to be fair I added the usual caching setup:

  • Redis Object Cache
  • WP Super Cache (page cache to disk)
  • OPcache tuned
                            TTFB p50    TTFB p95    RPS
WordPress (full cache)        38 ms      142 ms    3,850
Hugo (static)                 11 ms       27 ms   14,200

With full caching, WordPress closes 90% of the gap. It’s still about 3.5x slower and serves 3.7x fewer requests per second, but on the happy path a visitor would hardly notice.

The trouble is everything off the happy path. Any cache miss (logged-in users, comments, custom endpoints) puts you back at the uncached numbers above. You’re also now maintaining a cache stack that has to keep working, because when it doesn’t, the site falls over.

Build times, attack surface and memory
#

Benchmark posts rarely cover these, but they matter just as much day to day.

Build and deploy time
#

Running hugo on this site takes 113 ms, and a full deploy (build plus rsync) takes about 5 seconds. Ghost has no build step, but migrations and upgrades need downtime windows. WordPress has no build step either, and every plugin or theme update gets applied to the live site.

Attack surface
#

  • Hugo: no running services apart from nginx. The disk just holds HTML.
  • Ghost: a Node.js process, a SQLite file and an admin UI at /ghost/.
  • WordPress: PHP-FPM, MariaDB, /wp-admin, XML-RPC, the REST API, plus whatever the plugins add.

Over 12 months on a honeypot VPS, /wp-login.php was the most requested URL we saw. A static site doesn’t have a login page to go after.

Memory at idle
#

                       RSS idle
WordPress (full stack)   420 MB
Ghost                    180 MB
Hugo (static)              0 MB   # nginx handles it

The 420 MB is the floor, not a theoretical figure. As soon as real traffic arrives, PHP-FPM workers take even a modest site well past 1 GB.

When each one makes sense
#

None of this means everyone should drop WordPress. It exists because content editors who aren’t developers need a UI, and that need is real.

  • WordPress, if non-technical people edit content every day and you’re going to run the cache stack properly.
  • Ghost, if you want a WYSIWYG editor, newsletters and a decent API, and you’re comfortable managing a Node app.
  • Hugo (or Astro, or Eleventy), if your writers are fine working with Markdown files and git, and you want the speed and the smaller attack surface without extra effort.

This blog runs on Hugo. We write the posts and run the servers ourselves, and we’d rather spend that time on infrastructure than on patching plugins.