Worker Mode
Worker mode runs persistent PHP processes that bootstrap once and then handle many requests, so the cost of starting PHP is paid a single time instead of on every request. Rather than tearing down and rebuilding PHP state per request, your application loads its autoloader, configuration, and database connections once and reuses them for the lifetime of the worker.
How it works
- Enable worker mode. Set
WORKER_MODE_ENABLED=trueand pointENTRY_FILEat your bootstrap script. This enables worker mode for all PHP workers in the pool. - Bootstrap once. PHP starts and runs the outer scope one time. Autoloader registration, configuration loading, database connections, and any other initialization code execute a single time.
- Enter the request loop. Call
oxphp_worker(callback). OxPHP begins dispatching incoming HTTP requests to your callback. - Reset between requests. Superglobals (
$_GET,$_POST,$_SERVER,$_COOKIE,$_FILES,php://input), output buffers, and response headers are reset automatically. A soft reset cleans per-request state while preserving bootstrapped resources in the outer scope. - Outer scope persists. Variables defined before
oxphp_worker(), static properties, database connections, and autoloaders remain available across all requests handled by that worker.
Worker mode changes routing behavior. All requests that do not match a static file on disk are dispatched to the worker instead of returning 404. See Routing for details.
Configuration
| Variable | Default | Description |
|---|---|---|
WORKER_MODE_ENABLED |
false |
Enable persistent worker mode. Accepts true, 1, yes. Requires ENTRY_FILE to point at a .php script |
ENTRY_FILE |
(unset) | Path to the worker bootstrap script. Resolved against DOCUMENT_ROOT when relative; .. segments and absolute paths are allowed (worker bootstraps living outside the public document root are a supported layout) |
WORKER_MAX_MEMORY_MIB |
0 |
Maximum PHP memory per worker in MiB before recycling. 0 = unlimited |
The legacy WORKER_FILE variable is still parsed (with a startup WARN) and behaves as WORKER_MODE_ENABLED=true ENTRY_FILE=$WORKER_FILE. New deployments should use the explicit pair; the legacy form will be removed in a future release.
For application-driven recycling, call OxPHP\Server\Worker::scheduleExit() from inside a request handler. The worker exits cleanly after the current request completes.
Writing a worker script
A worker script has two parts: the outer scope that runs once at startup, and the callback passed to oxphp_worker() that runs on every request.
<?php
// Outer scope: runs once at startup
require __DIR__ . '/../vendor/autoload.php';
$config = parse_ini_file(__DIR__ . '/../config/app.ini');
$db = new PDO($config['dsn'], $config['user'], $config['pass'], [
PDO::ATTR_PERSISTENT => true,
]);
$app = new MyApp\Application($config, $db);
// Request loop: runs for every request
oxphp_worker(function () use ($app) {
$app->handle();
});
// Shutdown: runs when the worker exits
$app->terminate();What resets and what persists
OxPHP performs a soft reset between requests. Per-request state is cleaned automatically, while anything bootstrapped in the outer scope survives for the life of the worker.
- Superglobals —
$_GET,$_POST,$_SERVER,$_COOKIE,$_FILES, andphp://inputare repopulated with the new request data - Output buffers — all output buffers are flushed and cleaned
- Response headers — HTTP status code and headers are reset to defaults
- Error state — last error information (message, file, line, type) and connection status are cleared. User-registered error handlers (
set_error_handler()), exception handlers (set_exception_handler()), and theerror_reporting()level persist across requests
- Variables in outer scope — anything defined before
oxphp_worker()and captured viause - Static properties — class static properties retain their values
- Database connections — PDO, MySQLi, and other persistent connections remain open
- Autoloaders — registered autoloaders (Composer, custom) remain active
- Loaded classes and functions — all previously loaded classes, interfaces, traits, and functions
Recycling
Workers are automatically recycled (restarted with a fresh PHP process) when any of the following conditions are met:
- Max memory exceeded — the worker's PHP memory usage exceeds
WORKER_MAX_MEMORY_MIBMiB - Application requested exit — the handler called
Worker::scheduleExit(). Useful for app-controlled hot reload, file-mtime-based reload, or per-request bootstrap re-execution - Consecutive errors — the worker encounters 3 consecutive handler failures (fatal errors, timeouts, or unhandled exceptions). Note that
exit()/die()calls are not counted as failures
When a worker is recycled, the PHP process terminates and a new one starts, re-executing the outer scope of the worker script. For memory-based and scheduled exit, the current request completes normally before the worker exits. For error-based recycling, the worker exits after the failed request.
Development reloading
Worker Mode persists bootstrap state (autoloader, DI container, DB connections) in memory, so opcache.validate_timestamps=1 alone is not enough to pick up changes to code that ran during the outer scope. For development loops there are two options:
- Recycle every request. Call
OxPHP\Server\Worker::current()->scheduleExit()at the end of every handler invocation (gated on aOXPHP_DEVenv flag, for example). The current request completes normally, then the worker exits and is respawned, re-executing the outer scope. This trades the worker-mode performance win for FPM-style reload semantics. It is the simplest and most reliable approach for active development. - Keep the worker warm, reload request handlers. Skip
scheduleExit()entirely, enableopcache.validate_timestamps=1, and keep your bootstrap minimal. Code loaded inside the request callback will be refreshed by OPcache on the next request; code loaded once in the outer scope will not. See OPcache and JIT → Development Settings for the full list of caveats.
Troubleshooting
Requests hang and never complete
If oxphp_worker() is never called in the bootstrap script, no requests are dispatched and every request waits indefinitely. Verify that your script calls oxphp_worker() unconditionally in the normal code path.
State leaks between requests
Variables defined inside the oxphp_worker() callback are cleaned up by PHP's garbage collector, but static properties and globals defined in the outer scope persist. If you see data from one request appearing in another, check for static properties or global variables that accumulate state across calls.
Fix: Reset static state explicitly at the start of each request callback, or avoid storing per-request state in statics.
Worker recycles immediately (memory limit)
The worker memory limit is checked after each request using PHP's reported memory usage. If your bootstrap phase allocates a large amount of memory (e.g. loading a large cache), the initial memory footprint may already be close to the limit.
Fix: Increase WORKER_MAX_MEMORY_MIB or defer large allocations to the first request.
Worker recycles immediately (error limit)
Three consecutive handler failures trigger a recycle. Check your application logs for exceptions or fatal errors occurring in the request callback.
Check: Look for errors in the access log or structured log output:
docker logs <container> 2>&1 | grep '"level":"error"'Database connection drops after idle
If your database server closes idle connections, reconnect attempts in the next request may fail. Use a connection pool that handles reconnection, or catch the exception and reconnect manually.
Docker example
services:
app:
image: ghcr.io/oxphp/oxphp:0.10.0
ports:
- "8080:80"
volumes:
- ./src:/var/www/html
environment:
- DOCUMENT_ROOT=/var/www/html/public
- WORKER_MODE_ENABLED=true
- ENTRY_FILE=/var/www/html/worker.php
- WORKER_MAX_MEMORY_MIB=128PHP API
Worker introspection and the worker entry point are exposed through the OxPHP\Server\Worker class.
<?php
$worker = OxPHP\Server\Worker::current();
$worker->serve(function () {
handleRequest();
});Legacy free functions (oxphp_is_worker, oxphp_worker_id, oxphp_worker) remain available and route through the same internal state. New code should prefer the class API.
The class also exposes runtime introspection useful for graceful self-recycling, observability, and health checks:
| Method | Returns |
|---|---|
Worker::isWorkerMode(): bool |
Whether the server is running in worker mode |
$worker->id(): int |
Stable per-thread worker ID |
$worker->startTime(): float |
Unix timestamp of when this worker started |
$worker->requestCount(): int |
Number of requests this worker has handled |
$worker->memoryUsage(): int |
Current memory_get_usage(true) for this worker |
$worker->rss(): int |
Current resident set size in bytes (Linux/macOS) |
$worker->maxMemoryBytes(): int |
Recycle threshold — WORKER_MAX_MEMORY_MIB × 1 MiB, or 0 when unlimited |
$worker->isExitScheduled(): bool |
Whether scheduleExit() has been called |
$worker->exitReason(): ?string |
null while running; "scheduled", "max_memory", or "error" once the worker is going down |
See OxPHP\Server\Worker for full signatures and worked examples.
PHP examples
Detecting Worker Mode
Use OxPHP\Server\Worker::isWorkerMode() to check whether the current process is running in worker mode. This is useful for writing code that works in both traditional and worker mode.
<?php
if (OxPHP\Server\Worker::isWorkerMode()) {
// Reuse a persistent connection
$redis = new Redis();
$redis->pconnect('redis', 6379);
} else {
// Traditional mode: connect per request
$redis = new Redis();
$redis->connect('redis', 6379);
}Symfony worker script
<?php
use App\Kernel;
require __DIR__ . '/../vendor/autoload.php';
$kernel = new Kernel('prod', false);
$kernel->boot();
oxphp_worker(function () use ($kernel) {
$request = Symfony\Component\HttpFoundation\Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);
});
$kernel->shutdown();Best practices
- Set
WORKER_MAX_MEMORY_MIB(e.g.128) so a leaking worker recycles automatically instead of consuming the host. Combine withWorker::scheduleExit()for application-driven recycling on top. - Avoid storing per-request state in static properties or globals. Since these persist across requests, leftover state from one request can leak into another.
- Validate the soft reset early. Add
Worker::current()->scheduleExit()to your handler under a development flag and exercise the application end-to-end. This catches state-leak bugs before you commit to long-lived workers. - Handle database idle timeouts. If your database driver disconnects after an idle period, catch the exception and reconnect, or use a connection pool that handles reconnection automatically.
- Keep the outer scope minimal. Only bootstrap what truly needs to persist: autoloaders, configuration, and shared services. Defer request-specific setup to the callback.
See also
- Routing — how worker mode plugs into URL routing
- Early Response — send the response immediately and continue background processing
- PHP Functions — full reference for
oxphp_worker(),oxphp_is_worker(), and other built-in functions - Configuration Reference — complete list of environment variables