WordPress vs Ghost vs Strapi vs Payload vs Directus, load tested

Same server, same load, same job for all five: serve the public list posts API with 10 posts in it. Each CMS ran with its usual database from a docker compose file, default production config, nothing tuned. The only thing that changed between runs was the CMS.

Results, best to worst

CMSHeld aboutp95 under 100 usersErrorsWhat ran out first
Payload 3 + Postgres360 to 490 rps, peak 547160 msNoneOne Node thread, server about 40% busy
Strapi 5 + Postgres260 to 290 rps240 msNone, but p95 near 10 s by the endOne Node thread, server about 40% busy
Directus 11 + Postgres175 to 220 rps320 msFast 503s from about 1,250 users, 93% failed overallOne Node thread, server about 35 to 40% busy
Ghost 6 + MySQL100 to 115 rps560 ms429s from about 3.5 minutes in, 90% failed overallOne Node thread, server about 37% busy
WordPress 6.8 + MySQL75 to 78 rps950 ms71% timed out after 10 sAll 4 cores at 100% from the first second

The setup

One fresh 4 vCPU / 8 GB server (shared vCPU) per run, with the CMS and its database in one docker compose stack. WordPress, Ghost and Directus use their official images. Strapi and Payload do not ship one, so those are the stock create-strapi-app project and the Payload with-postgres template, built in production mode. Every CMS got the same 10 posts on first boot, then 2,500 simulated users ramped up over 10 minutes, all requesting the list of posts. The compose files are public at github.com/DevMatei/stresix-showdowns, so anyone can rerun them.

What happened

Payload kept climbing until about 830 users and then held around 490 requests a second with zero errors. Strapi never returned a single error either, but it topped out near 290 requests a second, so past that point every extra user just waited longer, and by the end p95 was close to the 10 second timeout. Directus peaked around 220 useful requests a second and then started turning requests away. Ghost managed about 115. WordPress stopped growing at about 200 users and 76 requests a second, and from there most requests timed out.

What ran out

Payload, Strapi, Directus and Ghost all hit the same wall: one Node thread pinned at 100% of a core while the server as a whole sat at 35 to 45%. Node runs your JavaScript on one thread per process, so a single process can only use one of the four cores. WordPress was the opposite. Apache and PHP spread the work across every core, all 4 were at 100% within the first second, and it still served the fewest requests. No database used more than half a core, so the database was not the limit for any of them.

Would a bigger server help?

For the four Node CMSes, not on its own. Each one was using one core out of four, so 8 or 16 vCPUs would only add idle cores. Running one process per core (cluster mode, or several containers behind a proxy) on the same server is the fix to try first. WordPress is the one case where more CPU would help directly, because it really did use everything it had. Caching would help it too, and we left caching out on purpose.

Two footnotes: Directus 503s and Ghost 429s

Directus ships with a pressure limiter turned on. When its event loop falls behind, it answers 503 straight away instead of letting requests queue. That is why its raw peak reads 3,398 requests a second: most of those were fast 503s, and the useful number is the 175 to 220. Ghost started answering 429 Too Many Requests about 3.5 minutes into the run. All load in a run comes from one address, so a per address limit trips far earlier than it would with real visitors. Its Node thread was already at 100% by then, so it was near its ceiling either way.

Notes on fairness

Each CMS returns its own default response shape, so response sizes differ, and WordPress sends the most per post. That is part of what you get out of the box, so it is left as is. Nobody got tuned: no PHP-FPM changes, no Node cluster mode, no Redis, no CDN. This is "installed it and shipped it". If a setup looks wrong, open an issue on the repo and we will rerun it.

What this does not tell you

This is one read only API endpoint with 10 posts. It says nothing about the admin panel, writes, image handling or rendering themed pages, and real sites usually put a cache or CDN in front of all of this. Each CMS ran once, on shared vCPU. The load came from a second server in the same datacenter, so these numbers measure server capacity, not what a visitor across the world sees.

Frequently asked questions

Is Payload faster than Strapi?

In this test, yes. On the same 4 vCPU / 8 GB server with default configs, Payload held about 360 to 490 requests a second on the list posts API and Strapi about 260 to 290, both with no errors. Both were limited by one Node thread, so running more processes should raise both.

Why was WordPress the slowest?

It used all 4 cores from the first second and still served the fewest requests, about 76 a second. Every REST API request runs through PHP and WordPress with no cache, and this test added none. With WordPress, a page or object cache is usually the first thing to add.

Will a bigger server make Strapi, Payload, Directus or Ghost faster?

Not by itself. In these runs each one maxed out a single core and left about 60% of the server idle. Run more processes first, then measure again before paying for more cores.

Can I test my own CMS setup like this?

Yes. Point Stresix at a repository with a compose file and it starts every service on one fresh server, sends the load, and reports what ran out first, per service and per thread. Your first run is free.

Related guides

Stop guessing your specs. Start knowing them.

Point Stresix at a repo or Docker image and get a number you can deploy on, in one run. Your first run is free, no card.

Start a free load test