WordPress Worker Mode
ePHPm ships a WordPress worker-mode adapter: boot WordPress core and
all plugins once per worker thread, then serve requests in a loop — the
30–80 ms wp-settings.php bootstrap happens once per worker, not once per
request. The adapter lives at
github.com/ephpm/wordpress-worker
and provides the worker entrypoint bin/ephpm-wp-worker.
WordPress has no service container or reset contract, so this adapter is
opinionated: it resets the per-request state WordPress core recomputes from the
URL/headers/body ($wp_query, $wp, $post, superglobals, current user) and
leaves worker-lifetime state (hook registry, post types, rewrite rules, the
object cache) alone.
ePHPm’s PHP packages are distributed via their GitHub repositories (not
Packagist). Install them by adding each repo in the dependency tree as a
Composer vcs repository.
1. Install the adapter
In a Composer-managed WordPress root, add every ePHPm repo in the tree to
composer.json. The adapter depends on ephpm/worker, so both repos are
listed — Composer does not resolve a VCS dependency’s own VCS repositories
transitively, so each ePHPm package needs its own repositories entry:
// composer.json
{
"repositories": [
{ "type": "vcs", "url": "https://github.com/ephpm/wordpress-worker" },
{ "type": "vcs", "url": "https://github.com/ephpm/php-worker" }
],
"require": {
"ephpm/wordpress-worker": "^0.1"
}
}Both ephpm/wordpress-worker and its ephpm/worker dependency are tagged
v0.1.0, so ^0.1 resolves for each; each still needs its own repositories
entry because Composer does not resolve VCS repos transitively. Then:
composer updateThis installs the entrypoint at vendor/bin/ephpm-wp-worker. (The engine skips
#!/usr/bin/env php shebang lines in worker scripts, so Composer bin proxies
work directly as worker_script.)
2. Configure ePHPm
# /etc/ephpm/ephpm.toml
[server]
listen = "0.0.0.0:8080"
document_root = "/var/www/wordpress"
[php]
mode = "worker"
worker_script = "vendor/bin/ephpm-wp-worker"
worker_populate_superglobals = true # REQUIRED for WordPressworker_populate_superglobals = true is required: unlike Octane/PSR-15
adapters (which build their own request object from the Envelope), WordPress
assumes real $_GET/$_POST/$_SERVER/$_COOKIE superglobals. ePHPm
repopulates them per request through PHP’s normal treat_data path.
worker_script must resolve to a file under document_root, so the adapter
must be installed inside the WordPress root.
Sizing and recycling
A booted WordPress kernel is heavy (~40 MB per worker) — you may want an
explicit worker_count lower than the CPU-derived default:
[php]
worker_count = 4 # 0 = derive from cgroup quota (Linux) or CPU count [2, 32]
worker_max_requests = 10000 # recycle after N requests (0 = never; pure leak guard)Recycling matters more for WordPress than for container-based frameworks:
- Plugin/theme updates and
wp-config.phpedits do not take effect in already-booted workers. The old code stays loaded until the worker recycles (worker_max_requests) or you restart ePHPm. Restart after updates if you can’t wait for the recycle cycle. - A fatal inside a hook, or a plugin calling
exit()/die()mid-request (WordPress does this routinely — redirects,wp_die()), never wedges the server: ePHPm synthesizes the response from the SAPI headers and captured output, then recycles the worker with a clean boot.
Object cache
Pair worker mode with the
ephpm/cache-wordpress drop-in so
wp_cache_* calls hit the embedded KV store — the cache persists across
requests and workers, and replicates across nodes in cluster mode. See the
WordPress guide for drop-in installation.
Limitations
- Block themes render, but their CSS is emitted only on the first request a
worker serves. WordPress tracks what it has already printed in
worker-lifetime state —
$wp_styles/$wp_scripts(WP_Dependencies::$done) and the staticWP_Style_Engine_CSS_Rules_Store. On a persistent worker those survive the request, so the second and later requests print no<style>blocks at all: measured on WordPress 6.7.1 with Twenty Twenty-Five, the homepage drops from ~50 KB (21 inline stylesheets, includingglobal-styles-inline-cssandcore-block-supports-inline-css) to ~12 KB with zero.wp_unique_id()’s counter also keeps climbing, so the generatedwp-container-core-*-is-layout-Nclasses drift away from the CSS that was printed. Resetting those registries per request is the adapter’s job, not the engine’s — until the adapter does it, serve block themes in fpm mode. (The engine itself renders blocks fine:do_blocks(), nested template parts, the Query loop, the Interactivity API and REST all survive a persistent worker. This was previously reported as an engine crash in ephpm#116; it no longer reproduces.) - Worker mode is a whole-server switch; it is not supported with
[server] sites_dir(config load hard-errors), so multi-tenant vhosting — including WordPress multisite behind vhosts — stays on fpm mode for now. - Plugins that assume process death for cleanup (e.g.
pcntl_fork-based backup plugins) are not compatible with any worker-mode runtime. If a plugin misbehaves under worker mode, run that site in the default fpm mode — it remains fully supported and byte-for-byte identical to previous releases.
See also
- WordPress guide — classic (fpm-mode) deployment paths
- Config reference —
[php]— authoritative worker knobs - Metrics reference —
ephpm_worker_*series