Migrating from PHP-FPM to Persistent Workers
Migrating a production codebase from traditional PHP-FPM to persistent workers (such as FrankenPHP Worker Mode or Laravel Octane) can be intimidating. In PHP-FPM, applications relied on the operating system to clean up memory, database locks, and global variables after every request.
Leakless serves as your migration safety net, allowing teams to transition smoothly in three structured phases.
Phase 1: Static Pre-Migration Audit
Before enabling worker mode in staging or production, run the Leakless static analyzer across your entire codebase:
vendor/bin/leakless analyzeThe analyzer inspects your AST to identify code patterns that are dangerous in persistent workers:
- Process Terminators:
exit()ordie()calls that would kill the entire worker process and drop concurrent connections. - Native Sessions:
session_start()andsession_id()calls that corrupt persistent concurrency. - Direct Header Output:
header()andsetcookie()calls that bypass framework response objects and bleed headers into subsequent user requests. - Mutable Static Properties: Static variables retaining cache or user data across requests without the
#[AllowPersistentState]attribute. - Singleton Injections: Request-scoped services (like
RequestorSession) injected into long-lived singleton constructors.
Fix all flagged issues or wrap intentional static caches with #[AllowPersistentState].
Phase 2: Runtime Safety Net
When switching to persistent worker mode in staging or production, install themattosdev/leakless:
composer require themattosdev/leaklessLeakless provides an autonomous runtime guard:
- Uncommitted Transactions: If a legacy controller or third-party package fails to commit a database transaction, Leakless automatically detects the open PDO transaction, logs the violation, and executes a rollback before the next user request is handled.
- Unclosed Output Buffers: If legacy code calls
ob_start()withoutob_end_clean(), Leakless drains the buffer to prevent HTML leakage. - Native Memory Limits: If legacy image processing or external API calls leak memory via native C libraries, Leakless monitors real Linux kernel RSS and gracefully recycles the worker after the active request finishes.
Configure your .env to log all violations for developer triage:
LEAKLESS_ENABLED=true
LEAKLESS_LOG_VIOLATIONS=true
LEAKLESS_MAX_RSS_MB=256
LEAKLESS_MAX_REQUESTS=1000Phase 3: Test Suite Fortification
Add persistent worker assertions to your existing test suite using Pest:
use App\Services\LegacyInvoiceService;
test('legacy invoice service contains no mutable static state', function () {
expect(LegacyInvoiceService::class)->toBeLeakless();
});
test('critical checkout workflow runs with clean worker state', function () {
expect(function () {
$service = app(CheckoutWorkflow::class);
$service->processPendingOrders();
})->toRunCleanly(maxDriftMb: 2.0);
});And in Laravel feature tests:
test('api checkout endpoint maintains state integrity', function () {
$response = $this->postJson('/api/v1/orders', [
'items' => [1, 2, 3],
]);
$response->assertOk()
->assertNoDanglingTransactions()
->assertNoMemoryDrift(maxAllowedMb: 2.0)
->assertCleanWorkerState();
});