← All posts
The Daemon: warm workers, no boot cost

The Daemon: warm workers, no boot cost

Every PHP request traditionally pays a small tax before it does any real work: bootstrap the framework, wire up the app, then handle the request. FrankenPHP's worker mode already removes most of that by keeping the app resident in memory between requests. The Phlo Daemon takes the same idea and extends it to everything that is not a normal HTTP request.

What actually needs a daemon

Three things benefit from a process that is already warm and already running: Phlo Realtime (covered in an earlier post), the runtime helpers (phlo_sync, phlo_async, await, phlo_stream) when one request fans out into many calls, and scheduled tasks. The daemon is a pool of workers that can run any Phlo target, a route, a method, a task, without paying a fresh boot cost per invocation.

prop tasks => arr(
    cleanup: arr(do: 'session::prune', every: '15 minutes'),
    report:  arr(do: 'reports::sendWeekly', weekly: 'monday 08:00'),
)

There is no cron syntax in the app itself, only every:, daily: or weekly: against a plain schedule reader. That schedule lives in %app->tasks either way, with or without a daemon; what changes is the trigger. Without a daemon, one generic cron line calls tasks::run every minute and the resource itself decides what is actually due. With a daemon, every app in its host map gets that same minute tick built in, so the one cron line goes away too.

The runtime helpers work the same way: without the daemon constant set, phlo_async/await still run as one-shot background processes, each paying a fresh boot. Set daemon: 3001 in phlo_app(...) and the same calls route to the resident worker pool instead, no boot per call. The win is biggest exactly where one request fans out into many calls, await() over a hundred missing translations is a hundred boots on the one-shot path, a hundred dispatches on a warm pool with the daemon.

Optional by construction

Core Phlo does not require the daemon, and the two things it adds fail differently without one. Realtime genuinely has no fallback: no daemon means no WebSocket server, full stop. Tasks do have one: without a daemon, a single cron line calling tasks::run every minute does exactly what the daemon's built-in tick does, just with one crontab entry to manage instead of zero. That matters operationally: you can develop and deploy a Phlo app with no daemon at all, lean on cron for scheduling, and add the daemon later the moment Realtime or a fan-out-heavy request actually calls for it, without restructuring anything that already exists.

The shape of it in production

In a production layout, the daemon is a sidecar next to FrankenPHP, both wired up by the same server platform: the /server-setup builder gives FrankenPHP a real systemd unit and runs the daemon under pm2, itself registered with systemd so it resurrects on boot. It is one more warm process on the box, not a separate fleet to operate. The tuning that matters is worker count against available memory, the same tuning any resident-process runtime needs, and it scales the same way the rest of Phlo does: horizontally, with identical nodes behind a load balancer, each running its own daemon for its own app.

The point is not "cron is bad." It is that scheduled work and realtime work are close enough in shape, a background process that runs your code without a boot cost, that they deserve one answer instead of two unrelated ones.

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