Lewati ke konten
Rama's logo Qisthi Ramadhani
Go back

Laravel Octane Swoole Memory Leak in Production: How to Fix

TL;DR

Memory leaks in Laravel Octane with Swoole happen because the application stays in memory across requests instead of bootstrapping fresh on each request like PHP-FPM. Static properties, singletons registered in service providers, event listener accumulation, and improper use of global state all persist between requests, causing memory to grow continuously until the worker is killed. This guide walks you through identifying the symptoms, understanding the root cause, and implementing a production-tested fix with real code examples.


Symptoms

If you’re experiencing this issue, you’ll likely notice one or more of these signs in your application:

If any of these symptoms look familiar, you’re dealing with laravel octane swoole memory leak in production. Read on to understand why it happens and how to fix it properly.


Root Cause

Memory leaks in Laravel Octane with Swoole happen because the application stays in memory across requests instead of bootstrapping fresh on each request like PHP-FPM. Static properties, singletons registered in service providers, event listener accumulation, and improper use of global state all persist between requests, causing memory to grow continuously until the worker is killed. Common culprits include: logging to arrays, caching query results in static properties, adding event listeners in middleware that fire on every request, and third-party packages that assume a fresh process per request.

Understanding the root cause is critical before applying a fix — treating only the symptoms often leads to the problem resurfacing in a different form, especially under production load.


How to Fix

Follow these steps in order. Each step builds on the previous one, and skipping steps may result in an incomplete fix.

Step 1: Enable Octane’s built-in memory leak detection: set OCTANE_MAX_REQUESTS=500 to auto-restart workers and confirm the leak exists by monitoring memory over time

Enable Octane’s built-in memory leak detection: set OCTANE_MAX_REQUESTS=500 to auto-restart workers and confirm the leak exists by monitoring memory over time

Step 2: Profile memory with Swoole Tracker or Xdebug: run php -d memory_limit=512M artisan octane:start and watch memory per worker

Profile memory with Swoole Tracker or Xdebug: run php -d memory_limit=512M artisan octane:start and watch memory per worker

Step 3: Audit service providers: ensure any singleton bindings that store request-specific state are flushed

Audit service providers: ensure any singleton bindings that store request-specific state are flushed — register them in Octane’s ‘flush’ array in config/octane.php

Step 4: Check for static property accumulation: search your codebase for static $ and ensure these are reset between requests

Check for static property accumulation: search your codebase for static $ and ensure these are reset between requests

Step 5: Review event listeners: move listeners from middleware to EventServiceProvider and ensure Octane flushes them

Review event listeners: move listeners from middleware to EventServiceProvider and ensure Octane flushes them

Step 6: Add leaking classes to Octane’s warm/flush configuration in config/octane.php

Add leaking classes to Octane’s warm/flush configuration in config/octane.php

Step 7: Use Octane’s RequestTerminated listener to manually clean up resources if needed

Use Octane’s RequestTerminated listener to manually clean up resources if needed


Code Example

The following before/after comparison shows the core pattern you need to change. Pay attention to the comments explaining why each change matters.

Before (Broken)

// LEAKS: Static cache accumulates across requests in Octane
class PricingService
{
    private static array $cache = [];

    public function getPrice(int $productId): float
    {
        if (!isset(self::$cache[$productId])) {
            self::$cache[$productId] = DB::table('prices')
                ->where('product_id', $productId)
                ->value('amount');
        }
        return self::$cache[$productId]; // grows forever in Octane
    }
}

After (Fixed)

// FIXED: Use Octane-aware caching with bounded size
use Laravel\Octane\Facades\Octane;

class PricingService
{
    public function __construct(
        private CacheManager $cache
    ) {}

    public function getPrice(int $productId): float
    {
        return $this->cache->remember(
            "price:{$productId}",
            now()->addMinutes(5),
            fn () => DB::table('prices')
                ->where('product_id', $productId)
                ->value('amount')
        );
    }
}

// config/octane.php — flush singletons between requests
'flush' => [
    PricingService::class,
],

Key Takeaways


Difficulty Level

Advanced

This guide requires deep understanding of framework internals and production debugging techniques. You should be experienced with profiling tools, understand concurrency patterns, and be comfortable debugging issues that only manifest under production load or specific timing conditions.


Frequently Asked Questions

How do I find which class is leaking memory in Laravel Octane?

Use Swoole Tracker or run php -r "echo memory_get_usage();" before and after handling requests. In development, add logging to Octane’s RequestReceived and RequestTerminated events to track memory delta per request. The class with the largest delta is likely your leak. Also search for static $ properties in your codebase — these are the most common Octane leak source.

Start with —max-requests=1000. This automatically restarts workers after 1000 requests, preventing runaway memory leaks from crashing your server. If your memory profiling shows no leaks, you can increase this to 10000 or higher. If you see rapid memory growth, lower it to 500 while you investigate.


Share this post on:
LLM-friendly version:
Open in ChatGPT Open in Claude

Related Posts


Previous Post
Laravel Horizon Redis Cluster Queue Management Issues: How to Fix
Next Post
Laravel Queue Deadlock with Redis vs Database Driver: How to Fix