FrankenPHP is a modern PHP application server built on top of Caddy. Its worker mode changes the fundamental execution model: instead of bootstrapping your application for every request and tearing it down afterward, the application boots once and handles multiple requests in a persistent process. This eliminates the per-request bootstrap cost, which is the most expensive part of most PHP request cycles.
For legacy PHP applications, worker mode is both an opportunity and a minefield. The performance gains are real, but the compatibility requirements are strict. Code that works fine under PHP-FPM, where every request starts with a clean slate, can break subtly in worker mode where state persists between requests.
This guide covers the practical reality of running legacy PHP on FrankenPHP workers: what you gain, what breaks, how to fix it, and when to stay on PHP-FPM instead.
How Worker Mode Differs from PHP-FPM
In the traditional PHP-FPM model:
- A request arrives
- PHP-FPM assigns a worker process
- The worker runs your
index.phpfrom the top - Your framework boots: autoloader, configuration, service container, routing, middleware
- Your handler runs the business logic
- The response is sent
- PHP tears everything down: objects are destroyed, memory is freed, the process resets
In FrankenPHP worker mode:
- Your application boots once at process start
- The framework loads, the service container is built, routes are registered
- A request arrives
- The worker processes the request using the already-booted application
- The response is sent
- The worker waits for the next request
- Steps 3 to 6 repeat indefinitely
The bootstrap in step 2 happens once, not per-request. On a framework like Laminas MVC or Symfony, the bootstrap can take 30 to 100ms. Eliminating it on every request is a significant performance win.
What You Gain
Eliminated Bootstrap Cost
For a typical Laminas MVC application, the bootstrap includes:
- Autoloader initialization
- Configuration merging from multiple files
- Service manager setup with hundreds of factory definitions
- Route compilation
- Event manager wiring
- Module loading
In PHP-FPM, this happens on every request. In worker mode, it happens once. On benchmark tests, this typically reduces average response time by 30% to 60% for framework-heavy applications.
Better Memory Efficiency
Because the framework state is shared across requests, the per-request memory allocation is lower. Each request only allocates memory for request-specific data, not for the entire framework bootstrap.
Lower Latency Variance
With PHP-FPM, the first request to a cold worker is slower because it triggers OPcache population. In worker mode, the application is already warm.
What Breaks in Legacy Code
Global State
This is the number one killer. PHP-FPM’s share-nothing architecture means every request starts clean. Legacy PHP code relies on this heavily, often without developers realising it.
Common global state problems in worker mode:
Static properties that accumulate data:
1 | class Logger |
In PHP-FPM, $messages is empty at the start of every request. In worker mode, it grows across requests until memory is exhausted.
Singleton instances that cache request-specific data:
1 | class Auth |
In worker mode, the singleton persists between requests. If request 1 authenticates as User A, the singleton still holds User A when request 2 arrives. This is a security vulnerability.
Global variables and superglobals:
1 | $GLOBALS['config'] = loadConfig(); |
These do not reset between requests in worker mode.
Memory Leaks
In PHP-FPM, memory leaks are invisible because the process resets after each request. In worker mode, a 100 KB leak per request grows to 100 MB after 1,000 requests. Your application will eventually run out of memory.
Common leak sources in legacy code:
- Event listeners that register on every request without deregistering
- Arrays that append without bounds
- Closures that capture large objects
- Circular references that the garbage collector does not clean efficiently
Database Connection State
Long-lived database connections can accumulate state:
- Transaction locks that were not committed or rolled back
- Session variables set by one request visible to the next
- Prepared statement handles that exhaust server-side limits
File Handles and Resources
Open file handles, stream resources, and curl handles that are not explicitly closed persist across requests and can exhaust system limits.
Making Legacy Code Worker-Compatible
Audit for Global State
Search your codebase for:
1 | grep -rn 'static \$\|self::\$\|static::' src/ app/ |
Each match needs evaluation. Not every static property is a problem. Immutable configuration values that are set once during bootstrap are fine. Mutable state that changes per request is not.
Reset State Between Requests
FrankenPHP provides a request lifecycle hook. Use it to reset state:
1 | // Worker script |
For each stateful class, add a reset() method that clears request-specific data.
Use Dependency Injection Instead of Singletons
The permanent fix for singleton problems is to stop using singletons. Use the service container to manage instance lifetimes:
1 | // Instead of Auth::getInstance() |
In worker mode with a properly configured container, request-scoped services are created fresh for each request while application-scoped services persist.
Implement Memory Monitoring
Add a memory check to your worker loop:
1 | $memoryLimit = 128 * 1024 * 1024; // 128 MB |
This is the worker-mode equivalent of pm.max_requests in PHP-FPM.
When to Stay on PHP-FPM
Worker mode is not always the right choice. Stay on PHP-FPM when:
- Your codebase has extensive global state that would take weeks or months to refactor. The compatibility work outweighs the performance benefit.
- You rely on third-party libraries that use static state internally. You cannot easily reset state you do not control.
- Your application has highly variable request patterns where some requests are fast and others run for minutes. Long-running requests in worker mode block the worker from handling other requests.
- Your deployment process requires zero-downtime and you are not ready to handle worker process recycling during deploys.
The PHP-FPM Tuning by Measurement guide covers how to get the best performance from PHP-FPM when worker mode is not suitable.
Deployment Considerations
Graceful Restarts
When you deploy new code, running workers have the old code loaded in memory. You need to restart them to pick up changes. FrankenPHP supports graceful restarts, but your deployment pipeline needs to account for the restart time.
Health Checks
Worker processes that get into a bad state (memory leak, stuck database connection, corrupted state) need to be detected and recycled. Implement health check endpoints that verify the worker’s internal state.
Monitoring
Monitor per-worker memory usage over time. A steady upward trend indicates a memory leak. A sawtooth pattern (rise then drop) indicates normal allocation and garbage collection.
FAQ
Can I run Laminas MVC in FrankenPHP worker mode?
It requires work. Laminas MVC’s service manager and event manager need careful state management between requests. It is possible but not trivial.
Is FrankenPHP production-ready for legacy applications?
FrankenPHP itself is production-ready. Whether your legacy application is ready for worker mode depends on how much global state it uses. Test thoroughly.
What about RoadRunner or Swoole?
They solve the same problem with different architectures. The compatibility requirements are similar: your code must handle persistent state correctly. The specific APIs differ.
Next Steps
If you are considering worker mode, start by auditing your codebase for global state. Fix the most obvious issues (singletons, static accumulators, superglobal access), then run your test suite in a worker-mode environment. The gap between “tests pass under PHP-FPM” and “tests pass under worker mode” reveals the compatibility work ahead.
The PHP Performance Playbook covers the measurement methodology you need to quantify the actual performance gain from worker mode versus the optimised PHP-FPM configuration described in the Performance Optimisation chapter.