Pext cluster mode: a drop-in replacement for php-fpm behind Apache
Pext-fpm is the latest SAPI in the Pext runtime, and it does the same job php-fpm does in a PHP deployment: sit behind a web server, accept FastCGI-encoded requests, and hand them off to a pool of worker processes that run application code. The point of the exercise is to slot pext-fpm in wherever php-fpm used to sit, with Apache (or Nginx) configuration and .htaccess files completely unchanged. Same protocol on the socket, same process model behind it, same operational shape.
What php-fpm actually does
Php-fpm is a small daemon that owns a FastCGI socket and a pool of child processes. When a request arrives on the socket, php-fpm hands the encoded envelope to a free worker; the worker decodes it, sets up the PHP request scope, runs the script, sends the response back through the socket, tears down the request scope, and reports itself free again. The web server in front of it (Apache with mod_proxy_fcgi, Nginx with fastcgi_pass) does not know anything about PHP; it just speaks FastCGI to whatever daemon is listening on the configured socket.
The important properties fall out of that shape. Configuration is external: the web server's ProxyPassMatch or fastcgi_pass line, together with .htaccess, decides which requests go to the FastCGI backend and how. The backend is a black box that speaks a wire protocol. Swapping php-fpm for something else is a matter of swapping the daemon on the socket while leaving every other piece of configuration where it is. That is the swap pext-fpm makes possible.
Node cluster as the process-pool primitive
Node's cluster module gives you exactly the shape php-fpm needs: a master process that owns a listening socket, a set of worker processes forked off the master, and a load-distribution scheme (round-robin by default) that dispatches incoming connections across the workers. Each worker is a normal Node process with its own event loop and its own memory; nothing is shared. That maps cleanly onto PHP's shared-nothing model: one worker handles one request at a time, tears down the request scope, and moves to the next request without leaking state.
Pext-fpm's master process starts by binding the FastCGI socket and forking --workers=N children off itself. Each child re-enters the Pext bootstrap with a per-worker context, then loops on the socket accepting connections. Because the socket is owned by the master, the OS-level load balancing across workers is done by the kernel's socket dispatcher, not by any application-level scheduler. That is the same design php-fpm uses, and it is why the model composes cleanly with any FastCGI-speaking web server.
One non-obvious quirk surfaced during bring-up: Node's cluster IPC uses an internal queryServer handshake to attach a worker to the master's listening socket, and that handshake is not reentrant. Firing off N parallel cluster.fork() calls at startup hits an EADDRINUSE from the second worker onward. Serialising the initial fork burst (fork one, wait for the ready signal, fork the next) is the fix, and after the pool is warm the fast path is fully parallel.
FastCGI on the wire, Pext behind it
The FastCGI protocol itself is not complicated: a binary framing on top of a stream socket, with typed records for BEGIN_REQUEST, PARAMS, STDIN, STDOUT, STDERR, and END_REQUEST. Pext-fpm ships a small codec in fcgi/codec.ts that reads and writes those records off the socket, and a protocol module in fcgi/protocol.ts that stitches them together into a request envelope. From the perspective of the worker, a request looks like a Request object with the same shape it would have under the CLI-server SAPI: parameters, headers, body stream, environment.
Once the envelope is decoded, the worker calls into the same request-lifecycle path every other Pext SAPI uses. Bootstrap, scope setup, script execution, output capture, RSHUTDOWN, response emission. The FastCGI codec on the way out re-encodes stdout as STDOUT records and closes the connection with an END_REQUEST. Nothing about the application code is different from the CLI-server case; the only difference is who is on the other end of the socket.
One Apache-specific quirk did require a small fix. When Apache's mod_proxy_fcgi forwards a request with a ProxyPassMatch destination, it prepends the destination URL to the SCRIPT_FILENAME parameter it sends, producing values like proxy:fcgi://127.0.0.1:9000/var/www/index.php. The FastCGI codec now strips the proxy:fcgi://host:port prefix so the worker sees the plain filesystem path it expects.
Apache mod_proxy_fcgi in front, .htaccess intact
The Apache side is stock. The recipe is: enable mod_proxy and mod_proxy_fcgi, add a ProxyPassMatch line pointing .php URLs at the pext-fpm socket, keep every other configuration unchanged. Rewrite rules in .htaccess keep working because Apache handles them before the request ever reaches the FastCGI backend. Directory-level PHP settings that used to live in php_admin_value map onto pext-fpm's equivalent, and everything else is genuinely untouched.
The verification for this work has been a three-way side-by-side deployment of NanoCMS: the same source served through mod_php in Apache, through pext-fpm behind Apache's mod_proxy_fcgi with four workers, and through the embedded pext -S server. All three serve the same content, and the pext-fpm path handles the same set of Apache rewrite rules the mod_php path does. For any operational team whose muscle memory is Apache plus .htaccess plus php-fpm, pext-fpm is the same shape.
Where this lands today
Pext-fpm ships now, with 28 tests around the FastCGI codec and protocol layers and 112 tests in the surrounding bootstrap suite. The master and worker code is in runtime/bootstrap/src/fpm/, the FastCGI codec is in runtime/bootstrap/src/fcgi/, and the entry point is bin/pext-fpm. The intended deployment shape is the one described above: put it where php-fpm used to sit, keep the web server configuration, run the workers, watch the output.
The next piece of work on this SAPI is a graceful reload path for zero-downtime deploys: send the master a signal, have it start a new worker generation, drain the old workers, close the socket handoff cleanly. Php-fpm has this and the operational muscle memory around deploys assumes it. Once that lands, pext-fpm is genuinely a like-for-like swap for any team running PHP behind Apache today.