Why serving WordPress doesn't need PHP
A WordPress page view does a remarkable amount of work. A PHP worker wakes up, loads WordPress core, your theme and every active plugin, opens a connection to a MySQL server, runs the loop, and tears most of it down again. For every single request.
None of that work is what makes your site your site. Your site is the content, the theme that renders it, and the admin you edit it with. BookPress keeps exactly those three things and replaces everything underneath.
There is one running that you can use. trybookpress.dev is the
kadence theme with WooCommerce, Elementor, Yoast SEO, Contact Form 7, Yoast Duplicate Post and Hello Dolly all active, served by this
engine on a Google Cloud e2-micro with no page cache in front of
it. Sign in as demo / demo and write a post. It
resets every 30 minutes and everything on it is public, so put nothing
personal into it.
The engine
- Compiled, not interpreted. Native C++ built for modern x86-64 and arm64 Linux servers, with SIMD-accelerated request parsing and HTML generation. No interpreter, no opcode cache, no worker pool to warm up. The entire server is a 1.8 MB binary.
- Embedded storage, optimized for low latency. Content queries return in microseconds, with no database server in the request path. An idle site holds no server resources.
- An event-driven architecture instead of workers. One process serves thousands of sites. Density is measured in sites per server, not workers per site: the target is 10,000+ sites on a single machine.
Real theme rendering
BookPress renders WordPress block themes natively. It reads
the same template files, the same block markup, the same
theme.json, and produces the same HTML and CSS WordPress produces:
global styles, layout classes, font presets, fluid typography. The default
WordPress theme, Twenty Twenty-Five, renders pixel-close today from its
unmodified theme files, web fonts and all.
The admin works the same way. /wp-login.php verifies your
existing WordPress password hashes, and /wp-admin gives you the
dashboard, post lists, and editor, served by the same engine at the same
speed.
What we compared, and why it is a hard test
There are two benchmarks on this site and they answer different questions. The front page's is the official WordPress Docker image, with and without a page cache, on one machine — it is what a reader can reproduce in ten minutes from a clean checkout, and it is honest about being the easy version: it is not a stack a specialist host runs, so a specialist can dismiss any single number it produces in a sentence.
This page is the hard version, and it exists because that dismissal is fair.
So the WordPress side of this benchmark is tuned properly, by us, before we measure it. OPcache with JIT enabled and its file limit counted rather than guessed. A Redis object cache over a UNIX socket. MariaDB with a 4 GB InnoDB buffer pool. PHP-FPM pre-forked static, with the child count derived from a measured child. WooCommerce on HPOS. WordPress's visitor-fired cron turned off and driven by a system cron instead. Every element of it is published below, verbatim, read back out of the running stack rather than announced by the script that set it.
We then measured what our tuning was worth, which is the honest thing to do: on the WooCommerce page we took the object-cache drop-in and the OPcache settings back out and put them back again, reading the state out of the running stack both times. WordPress went from 24.31 to 26.24 pages per second on x86 — +7.9%. The run before this one took the same measurement on both machines and got +7.1% on x86 and +1.8% on ARM. This run re-took only the x86 half, so that ARM number is the previous run's and says so, rather than being carried forward as if it were this one's. The stock image already ships OPcache enabled, and the tiers the tuning adds are a few percent of a page. We report it because a reader is entitled to know we did not win by leaving their side broken.
The machines
Two Google Cloud instances, both of which anyone can rent and check:
a c2-standard-4 (4 vCPU of Intel Xeon at 3.10 GHz,
2 physical cores with two threads each, 16 GB RAM) and a
c4a-standard-4 (4 vCPU of Google Axion Neoverse-V2,
4 physical cores, 16 GB RAM). Both Debian 12,
both built with the same clang.
Read those two core counts together before reading any number. "4 vCPU" is not the same amount of machine on the two families, and an ARM number is only ever compared against a WordPress running on the same machine.
Capacity: the question a host actually asks
Not "how fast is one page" but "how many machines do I need, and what
happens to my visitors at peak". So every figure below is the highest
sustainable throughput at which the 99th-percentile response stayed under one
second with zero errors, driven by wrk against both engines
identically. The workload is the site home page in every cell.
Machines needed to serve 1,000 dynamic page views per second, rounded up:
| Workload | BookPress x86 | WordPress x86 | BookPress ARM | WordPress ARM |
|---|---|---|---|---|
| Blog or brochure, no plugins | 1 | 23 | 1 | 12 |
| Elementor | 29 | 30 | 12 | 15 |
| WooCommerce | 20 | 37 | 9 | 18 |
The underlying throughput, at the knee, with the quality at that point:
| Site | Engine | Pages/s | p50 | p99 | Errors |
|---|---|---|---|---|---|
| No plugins | BookPress | 4,811 | 1.6 ms | 3.2 ms | 0 |
| No plugins | WordPress, dynamic | 45.0 | 175.0 ms | 281.7 ms | 0 |
| Elementor | BookPress | 35.7 | 220.1 ms | 673.0 ms | 0 |
| Elementor | WordPress, dynamic | 33.9 | 117.4 ms | 134.1 ms | 0 |
| WooCommerce | BookPress | 50.3 | 317.1 ms | 716.8 ms | 0 |
| WooCommerce | WordPress, dynamic | 27.1 | 145.9 ms | 166.8 ms | 0 |
c2-standard-4, home page, four BookPress workers against 65
PHP-FPM children; wrk, peak sustainable throughput with p99 under
one second and zero errors. ARM figures in the ratio table below. These are a
different harness from the front page's and are not comparable with it
cell-by-cell: this one drives a 422-post site through a tuned nginx + PHP-FPM +
Redis + MariaDB stack, that one drives a two-post site through the stock
Apache/mod_php image.
Where we are weaker, and we are not going to bury it
Our tail is worse than WordPress's on the plugin pages. At the Elementor knee on x86 our 99th percentile is 673 ms against WordPress's 134 ms, even though we serve more pages a second. A visitor at the median gets a faster page from us; a visitor in the unlucky one percent waits longer than they would on WordPress. If your service level is written in p99 rather than in throughput, measure before you switch.
The Elementor cell is also the closest race on this page, and it was a loss until recently. Two benchmark runs ago a tuned WordPress was 1.13× faster than us there. The reason was structural: Elementor's page cost is almost entirely the execution of Elementor's own PHP, and everywhere else we win either by not running PHP at all or by answering a WordPress API call in C++ instead of interpreting the core PHP behind it. On Elementor neither advantage applies, so the engine is exposed directly.
What changed was finding that our engine re-hashed a plugin's boot source on every single request, which turned out to be 41% of the work on an Elementor page. Removing that, and the two more of the same shape behind it, took the cell from 0.88× to 1.05×. We are stating the history because a 1.05× is a narrow win that could move again, in either direction, and because the honest read is that on Elementor the two engines are close rather than that we are ahead.
The full ratio table
| Site | x86-64 | arm64 |
|---|---|---|
| No plugins | 107.0× | 121.4× |
| Elementor | 1.05× | 1.33× |
| WooCommerce | 1.86× | 2.15× |
The 107× figure is for a site with no plugins installed at all, and that condition travels with the number everywhere it appears. It is the brochure and blog case, which is most of the web, and it is where BookPress is doing what it was built to do: rendering a real block theme from real content with no interpreter in the request path at all.
Memory
Measured as proportional set size, summed across every process in each stack, at peak load. Proportional rather than resident because the WordPress stack is 65 PHP-FPM processes sharing one binary and one opcache mapping, and summing resident memory over that pool triple-counts it in our favour.
| Site | BookPress | WordPress stack | Ratio |
|---|---|---|---|
| No plugins | 12.2 MB | 1,920.4 MB | 157× |
| Elementor | 133.0 MB | 2,080.3 MB | 15.6× |
| WooCommerce | 191.1 MB | 2,353.2 MB | 12.3× |
Three things keep this honest. The WordPress figure is the whole stack at peak load — nginx, all 65 PHP-FPM children, Redis and MariaDB — and a good part of it is configured limits rather than demand: a smaller box would be configured smaller. The plugin pages cost BookPress ten to fifteen times the plugin-free page, because the PHP interpreter and the plugin tree are resident; that is the one place our memory story is ordinary rather than extraordinary, and it is still an order of magnitude under the stack it replaces.
And the 157× is not the number on the front page, which says 271 MB against 14.0 MB — about 19×. Both are real and they have different denominators. The front page measures the stock Docker image: Apache, mod_php and MariaDB, one small site, no pool. This page measures the stack a host provisions to serve the same page at capacity. If you are sizing a fleet, this table is the relevant one; if you are checking us in ten minutes on a laptop, that one is.
No object cache, no database server, no page cache
The tiers a WordPress host assembles exist to keep PHP from doing work twice. A Redis object cache crosses a socket on every get and hands back bytes PHP must unserialize. A database server crosses a socket too. A page cache stores a rendered copy so the whole stack can be skipped.
BookPress has none of them. Storage is embedded and in-process, so there is no round trip to save. There is no rendered copy to keep, because rendering the page from scratch is already the fast path. A CDN is still worth having, for the reasons a CDN is always worth having: distance to your visitors, and static assets.
This is an operational claim before it is a performance one. Every tier is a thing that can be misconfigured, run out of memory, hold stale content, or need purging after a deploy. On this benchmark those tiers were worth about 5% of a WordPress page, which is the same finding our tuning delta shows from the other direction.
One measurement here goes against us and belongs on this page. We installed the Redis object cache plugin on BookPress too, on the principle that if a host already has it, disabling it to make our numbers look better would be dishonest. Our plugin engine activates it, and the site serves, but it cannot perform the plugin's actual function: the Redis client class and the raw socket primitives it needs are not available in our sandbox. So it falls back to WordPress's per-request cache and stores nothing across requests. On a plugin-free page, having it active costs 72% of our throughput, because activating any plugin at all moves a site onto the interpreter path. The honest sentence is that BookPress cannot run this particular plugin's function, not that we do not need one. Do not install an object cache on BookPress: it will cost you and give you nothing.
What a page cache does, and what it cannot do
With a page cache in front of it, WordPress served 20,000 to 37,000 requests a second in this benchmark, which is more than BookPress. That number is real and we are not going to hide it. It is also nginx returning a file from disk, with PHP never entered.
Its relevance is bounded by what it is allowed to serve. Not the cart, not the checkout, not the account page, nothing carrying a session cookie, nothing with a query string, nothing for a logged-in visitor. Those are precisely the pages that decide a store's capacity, and on those the cache steps aside and the request meets the PHP it was standing in front of. Every dynamic figure on this page is that moment.
The WordPress configuration, in full
Every element below was read back out of the running stack. Images pinned by digest.
wordpress:php8.3-fpm sha256:5a9cee044133c5bcafdd1232c31119c129ce75f86fc2c56097862c31924a605e
mariadb:11 sha256:2439dcd7d14010ecd1ff7a4e1c5abe8e208c34fe35290744deeeaac3569043c3
redis:7-alpine sha256:ff02b58f971e7d7d156a1267e283fcbbeee91773b6aa36c49dac28ecfe28eadf
nginx:1.27-alpine sha256:65645c7bb6a0661892a8b03b89d0743208a18dd2f3f17a54ef4b76fb8e2f2a10 The PHP image is the official one plus pecl install redis, because
it ships no redis extension and there is no object cache without one.
php.ini:
memory_limit = 512M
max_execution_time = 60
realpath_cache_size = 4096k
realpath_cache_ttl = 600
opcache.enable = 1
opcache.memory_consumption = 512
opcache.interned_strings_buffer = 64
opcache.max_accelerated_files = 100000
opcache.validate_timestamps = 0
opcache.save_comments = 1
opcache.fast_shutdown = 1
opcache.jit = tracing
opcache.jit_buffer_size = 256M max_accelerated_files is counted, not guessed: 7,671 PHP files
under the document root against a 100,000 limit. save_comments
stays on because WooCommerce and several Elementor paths read docblock
annotations.
PHP-FPM pool:
pm = static
pm.max_children = 64
pm.max_requests = 500
listen = /sock/php-fpm.sock
listen.backlog = 4096 The child count comes from a measured child, not a rule of thumb: mean
PHP-FPM child resident size 33 MB, so 9,500 MB available divided by 33
gives 288, capped at 64. The cap is a judgement rather than a measurement.
Past it, four vCPU is queueing rather than serving, and a host who sets 200
children on four cores has built a latency amplifier. pm = static
because a busy host pre-forks, which removes fork cost from the measurement in
WordPress's favour.
MariaDB:
--innodb-buffer-pool-size=4G --innodb-log-file-size=512M
--innodb-flush-log-at-trx-commit=2 --innodb-flush-method=O_DIRECT
--innodb-io-capacity=2000 --innodb-read-io-threads=4 --innodb-write-io-threads=4
--max-connections=200 --table-open-cache=8000 --tmp-table-size=128M
--max-heap-table-size=128M --skip-name-resolve Redis, over a UNIX socket:
redis-server --save "" --appendonly no --maxmemory 512mb
--maxmemory-policy allkeys-lru
--unixsocket /sock/redis.sock wp-config.php additions:
define( 'WP_REDIS_SCHEME', 'unix' );
define( 'WP_REDIS_PATH', '/sock/redis.sock' );
define( 'WP_CACHE', true );
define( 'DISABLE_WP_CRON', true );
define( 'WP_POST_REVISIONS', 5 ); Checked at measurement time, each of which can fail the measurement rather than warn about it: the object cache drop-in present with Redis connected and keys resident; OPcache reporting tracing JIT and 0% wasted; WordPress cron disabled with a system cron driving it; WooCommerce HPOS tables present; and the page cache reporting a bypass on every dynamic cell and a hit on every cached one.
Both engines served identical content, checked row by row: 422 posts, 601 comments, 73 terms, 73 taxonomies, 601 term relationships, equal on both sides.
The BookPress configuration, to the same standard
bookpress --root /srv/sites/<site> --db /srv/sites/<site>/data/site.db \
--port <port> --workers 4 No php.ini, no pool, no object cache, no opcode cache, no page cache, no reverse proxy. Every BookPress number on this page is a dynamic render.
Which architecture
ARM is about 1.4 to 1.5× faster per core, and the machine has twice the
real cores for the same vCPU count and roughly the same price. Across every
workload a c4a-standard-4 replaced 2.3 to 2.5
c2-standard-4s for BookPress. If you are choosing, choose ARM.
The front page's harness now runs on both machines too, and it isolates the
reason. Going from one worker to four buys 2.44× on the
c4a and 1.64× on the c2, on the
same client at the same settings — four real cores against two cores
presented as four threads. Every leg is faster on the c4a,
WordPress's included (its front page is 28.7 ms there against 42.3 ms
on the c2), so most of that column is the machine and not the
architecture of our build. The worker-scaling difference is the part that is
about our code, and even that is the SMT boundary rather than anything we
wrote. Our arm64 build carries no -march at all, because NEON is
baseline on AArch64; it is the same source at the same pin as the x86 one.
What this unlocks
When a plugin-free site costs 12 MB of memory across the whole server and a fraction of a millisecond of CPU per view, hosting economics invert. The marginal cost of one more idle site is measured, not estimated: 198 kB of memory. Thousands of low-traffic sites fit on one modest server and an idle site's cost approaches the cost of its storage.
We have not published prices yet. When we do they will be worked out from the infrastructure a host stops paying for, which is what the numbers on this page measure. The pricing page says how to get a figure for your own fleet in the meantime.