2: The worker pool

The daemon keeps a pool of resident PHP workers per app. Each worker boots the app once and then answers many calls, removing the per-call boot that the one-shot path pays. It is the same idea as FrankenPHP's HTTP worker mode, but for non-HTTP work.

2.1: The worker protocol

A worker runs php <app.php> phlo_serve, boots the app, and then reads newline-delimited JSON on stdin, one request in flight at a time:

in   {"id", "target", "args"?, "stream"?}
out  {"t":"ready"}                                            // once, after boot
     {"id", "t":"line", "data"}                               // 0..N, only when streaming
     {"id", "t":"done", "result"} | {"id", "t":"error", "message"}   // exactly one, terminal

Concurrency equals the pool size: a worker handles one call at a time, and the daemon queues the rest until a worker frees up.

2.2: Per-call reset

Between calls a worker resets state (phlo('tech/reset'), session close, GC), exactly like the FrankenPHP worker loop, so one call never leaks into the next. Write worker-safe targets: no request or user state in statics, and commit or roll back DB work. Long-lived connections you explicitly mark with objPers survive; everything else is cleared.

2.3: Dynamic sizing, recycle

The pool sizes itself: it spawns a worker when a call arrives and none is free, up to a global cap of one less than the core count, and reaps a worker once it sits idle. Nothing is configured.

Whether an app runs one-shot (a fresh process per call, which keeps hot-reload working) or on the resident pool (warm workers, no per-call boot) is configured per-host in config/daemon.js (or per-call for CLI dispatch): a build: true dev app is one-shot, a release app is pooled. Two guards keep a resident pool healthy: a per-call timeout (a stuck worker is killed and respawned) and recycling (a worker is replaced after a number of calls, to shed any slow leak). After a deploy, restart the workers so they reload the new build.

2.4: Health

GET /health on the daemon reports the live worker total against the cap, each pool keyed by app path, the connected sockets per host, and the configured hosts:

{
	"status": "ok",
	"workers": 5,
	"cap": 7,
	"pools": { "/var/www/app/release/www/app.php": { "workers": 4, "busy": 1, "queued": 0 } },
	"sockets": { "app.example.com": { "tokens": 12, "sockets": 18 } },
	"registered": ["app.example.com", "dev.example.com"]
}

busy is the workers currently running a call and queued the calls waiting for a free worker; workers/cap is the live total against the ceiling. A busy that sits at the cap with a growing queued means the node is at capacity: add nodes or give it more cores.

Last updated on 21 September 2026

We use essential cookies to make this site work. With your permission we also use analytics to improve the site.