How do I debug memory leaks under Laravel Octane (FrankenPHP)?
Question
We run Laravel Octane with the FrankenPHP driver at `max-requests=500`. Under heavy traffic the workers' memory climbs fast, RAM balloons, and the server gets close to locking up. PHP-FPM clears memory after every request, but Octane is stateful — singletons, static variables, and third-party packages cause this leak. How do I debug it, and how do I optimize memory while keeping workers persistent?
Answer
Short answer: don’t guess, measure — in Octane the worker is long-lived, so anything that accumulates between requests leaks; track what’s piling up first, then reset the stateful side in the hooks.
The real issue: PHP-FPM assumes “a fresh process per request,” but Octane keeps the worker alive. Everything that breaks that assumption — growing static arrays, singletons holding request state, listeners/bindings registered per request, global stores (Log, Auth, the current request) that never reset — quietly inflates memory.
- Measure first, talk later. Log
memory_get_usage(true)every N requests; if memory climbs monotonically you have a leak, if it oscillates and drops it’s normal. Drop a counter into Octane’sRequestTerminatedhook and look at the shape of the curve. If it trends upward, move to the next step. - Bisect to find the culprit. Disable service providers and suspect packages one at a time and re-measure; wherever the growth stops, that’s your leak. The offender is usually a third-party package that assumes a fresh process per request — invisible under PHP-FPM, explosive under Octane. Use a snapshot diff (the delta of
memory_get_usagebetween two points) to narrow which type is accumulating. - Reset stateful singletons in the hooks. Clear bindings that hold request state in the
RequestReceived/RequestTerminatedhooks, or via theflushlist inconfig/octane.php. Leaving the singleton in the container but resetting the state inside it closes most leaks in a single line. - Kill static accumulation, rebind services per request. Ever-growing static
$cache = []arrays, connections that never close, collections that swell per request — all classic. Don’t make a request-scoped service a singleton; rebind it each request so the data it carries dies with the request. - Treat
max_requestsas a safety valve, not a cure. Recycling the worker after a set number of requests caps RAM but hides the root cause — lowering500just moves you from crashing fast to crashing late. Keep it on, but do the actual work by closing the leak.
Bottom line: if it were me, I’d order it like this — plot the curve with memory_get_usage first, narrow the culprit by bisecting service providers/packages, then reset stateful singletons in Octane’s hooks and clear static accumulation. Keep max_requests as a net but don’t lean on it. If you want Octane’s speed, you have to accept “the process doesn’t die per request” in every layer of your code.
Related Reading
Comments
Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.