Skip to content
Muhammet Şafak
tr
Asked by: Yiğit Answered:

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.

Short answer

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. I covered what the persistent-process model changes in the Laravel Octane post.

Why

  1. A leak here isn’t a bug, it’s a broken assumption. Under PHP-FPM every request was born and died in its own process, and leaky code died with it. Octane removes that safety net and the same code starts accumulating.

  2. The culprit usually isn’t your code. A third-party package that assumes a fresh process per request is invisible under PHP-FPM and explosive under Octane. That’s why this needs measurement, not reading.

  3. max_requests hides the root cause. Recycling the worker after a set number of requests caps RAM but doesn’t close the leak — lowering 500 just moves you from crashing fast to crashing late.

What to do

  1. 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’s RequestTerminated hook and look at the shape of the curve.

  2. 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. A snapshot diff (the delta of memory_get_usage between two points) narrows which type is accumulating.

  3. Reset stateful singletons in the hooks. Clear bindings that hold request state in the RequestReceived/RequestTerminated hooks, or via the flush list in config/octane.php. Leaving the singleton in the container but resetting the state inside it closes most leaks in a single line.

  4. 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.

  5. Keep max_requests on as a safety valve. But do the actual work by closing the leak; don’t lean on it.

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

Share:

Comments

Sign in with your GitHub account to join the discussion. Comments are stored in GitHub Discussions.

More Questions

All questions

Search the site

Start typing to search posts, projects and pages.

Esc to close Powered by Pagefind