📁 Volume II: Laravel Performance Tuning Kit

🚀 Topic 21: Laravel Octane

The nuclear option. Keep Laravel alive in memory.

"Laravel was designed to die after every request.
Bootstrap. Execute. Die. Bootstrap again. Execute again. Die again.
Octane keeps Laravel ALIVE in memory.
Bootstrap once. Handle thousands of requests.
The result? 10-100x faster response times."
⚠️ THE NUCLEAR OPTION

Octane is not for everyone. It eliminates 90% of Laravel's bootstrapping overhead, but introduces new challenges: memory leaks become permanent, global state becomes dangerous, and you can no longer rely on "fresh" application state per request. Only use Octane if you understand these trade-offs.

🔴 The Problem: Laravel Dies After Every Request

TRADITIONAL PHP-FPM ARCHITECTURE ═══════════════════════════════════════════════════════════════════ Request 1: ┌─────────────────────────────────────────────────────────────────┐ │ Boot Laravel (30ms) → Execute Code (10ms) → Die (0ms) │ │ └─────────────────────────────────────────────────────────────────┘ Request 2: ┌─────────────────────────────────────────────────────────────────┐ │ Boot Laravel (30ms) → Execute Code (10ms) → Die (0ms) │ └─────────────────────────────────────────────────────────────────┘ Request 3: ┌─────────────────────────────────────────────────────────────────┐ │ Boot Laravel (30ms) → Execute Code (10ms) → Die (0ms) │ └─────────────────────────────────────────────────────────────────┘ Time breakdown: • Bootstrapping: 75% of request time • Actual work: 25% of request time • Huge waste of CPU! OCTANE (SWOOLE / ROADRUNNER) ARCHITECTURE ═══════════════════════════════════════════════════════════════════ Application Start: ┌─────────────────────────────────────────────────────────────────┐ │ Boot Laravel ONCE (30ms) → Application stays ALIVE │ └─────────────────────────────────────────────────────────────────┘ Request 1: Execute Code (10ms) → Response Request 2: Execute Code (10ms) → Response Request 3: Execute Code (10ms) → Response Time breakdown: • Bootstrapping: 0% of request time (done once) • Actual work: 100% of request time • 75% reduction in response time!
THE BOTTOM LINE

Octane makes Laravel 5-10x faster for most operations by eliminating the bootstrap overhead. A 50ms Laravel request becomes 5-10ms. A 200ms request becomes 20-30ms.

🔧 Swoole vs RoadRunner (Octane Servers)

🟢 Swoole

  • PHP extension (C-level)
  • Extremely fast
  • Built-in HTTP/2 support
  • WebSocket support
  • More features (coroutines, task workers)
  • Requires installing Swoole extension
  • Best for: Maximum performance

🔵 RoadRunner

  • Go binary (no extension needed)
  • Very fast (slightly slower than Swoole)
  • Easy to install (single binary)
  • Works with any PHP version
  • More stable for some workloads
  • Better for shared hosting
  • Best for: Ease of setup
INSTALLATION
# Install Laravel Octane
composer require laravel/octane

# Install Swoole (recommended for performance)
composer require swoole/php-ext
sudo pecl install swoole

# OR install RoadRunner (easier)
php artisan octane:install --server=roadrunner

# Start Octane
php artisan octane:start --server=swoole --port=8000

# Run with multiple workers
php artisan octane:start --server=swoole --workers=4 --task-workers=2

📊 Performance Comparison: PHP-FPM vs Octane

Operation PHP-FPM Octane (Swoole) Improvement
Simple Hello World 15-25ms 1-2ms 10-15x faster
Eloquent SELECT (100 rows) 30-50ms 5-10ms 4-6x faster
Complex page with views 80-150ms 15-30ms 4-5x faster
API endpoint (JSON) 20-40ms 3-8ms 4-6x faster
Requests per second (RPS) 50-200 RPS 500-2000 RPS 5-10x more capacity
REAL-WORLD EXAMPLE

A typical Laravel e-commerce site:

💧 The Memory Leak Danger (Why Octane is Dangerous)

MEMORY LEAK IN PHP-FPM vs OCTANE ═══════════════════════════════════════════════════════════════════ PHP-FPM (with pm.max_requests=500): ┌─────────────────────────────────────────────────────────────────┐ │ Request 1-500: Memory grows slowly (leak) │ │ Request 501: Process dies, memory freed, new process starts │ │ Result: Leaks are contained, server stays healthy │ └─────────────────────────────────────────────────────────────────┘ Octane (persistent process): ┌─────────────────────────────────────────────────────────────────┐ │ Request 1: Memory = 50MB │ │ Request 1000: Memory = 80MB (30MB leak) │ │ Request 10,000: Memory = 350MB (300MB leak) │ │ Request 100,000: Memory = 3GB (server crashes) │ │ Result: Leaks accumulate FOREVER until server dies │ └─────────────────────────────────────────────────────────────────┘ WHAT CAUSES MEMORY LEAKS IN OCTANE? ═══════════════════════════════════════════════════════════════════ • Singletons that accumulate data (e.g., array push on each request) • Static properties that never get cleared • Global variables that persist across requests • Database connection pools that grow unbounded • Caches that never expire • Event listeners that register new listeners each request
⚠️ CRITICAL WARNING

Code that works perfectly in PHP-FPM can leak memory catastrophically in Octane. Test your application thoroughly with Octane before deploying to production. Monitor memory usage closely.

✅ Octane-Safe Code Patterns

👎 DANGEROUS (Memory Leak)

// Static property accumulates data
class UserService
{
    private static array $processed = [];
    
    public function process(User $user)
    {
        self::$processed[] = $user->id;  // GROWS FOREVER
    }
}

// Singleton accumulating data
$this->app->singleton(Analytics::class, function () {
    return new Analytics();  // Events accumulate!
});

// Global variables
$GLOBALS['counter'] = ($GLOBALS['counter'] ?? 0) + 1;

👍 SAFE (No Leak)

// Use request-scoped data
class UserService
{
    public function process(User $user)
    {
        $processed = [];  // Fresh per request
        $processed[] = $user->id;
    }
}

// Use cache with TTL (not memory accumulation)
Cache::remember('counter', 60, function () {
    return Counter::increment();
});

// Avoid globals entirely
OCTANE BEST PRACTICES

⚙️ Octane Configuration

config/octane.php
return [
    // Server type (swoole or roadrunner)
    'server' => env('OCTANE_SERVER', 'swoole'),
    
    // Number of workers (CPU cores * 1-2)
    'workers' => env('OCTANE_WORKERS', 4),
    
    // Number of task workers (for async tasks)
    'task_workers' => env('OCTANE_TASK_WORKERS', 2),
    
    // Max request size (bytes)
    'max_request_size' => env('OCTANE_MAX_REQUEST_SIZE', 10 * 1024 * 1024),
    
    // Listen on all interfaces
    'host' => env('OCTANE_HOST', '0.0.0.0'),
    
    // Port (use 80 with reverse proxy)
    'port' => env('OCTANE_PORT', 8000),
    
    // Reload workers after N requests (like pm.max_requests)
    'max_requests' => env('OCTANE_MAX_REQUESTS', 1000),
    
    // Watch for file changes (disable in production)
    'watch' => env('OCTANE_WATCH', false),
    
    // Swoole specific
    'swoole' => [
        'options' => [
            'enable_coroutine' => false,  // Disable for Laravel
            'open_http2_protocol' => true,
            'http_compression' => true,
        ],
    ],
    
    // RoadRunner specific
    'roadrunner' => [
        'options' => [
            'http' => [
                'middleware' => ['static', 'gzip'],
            ],
        ],
    ],
];

🛠️ Production Deployment with Octane

SUPERVISOR CONFIGURATION
# /etc/supervisor/conf.d/octane.conf

[program:octane]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan octane:start --server=swoole --workers=4 --task-workers=2 --max-requests=1000
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=1
redirect_stderr=true
stdout_logfile=/var/www/storage/logs/octane.log
stopwaitsecs=3600

[program:octane-watcher]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/artisan octane:reload
autostart=true
autorestart=true
user=forge
numprocs=1
NGINX CONFIGURATION (PROXY TO OCTANE)
# /etc/nginx/sites-available/example.com

server {
    listen 80;
    server_name example.com;
    
    # Proxy all requests to Octane
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_buffering off;
    }
    
    # Static files still served by Nginx
    location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2)$ {
        root /var/www/example.com/public;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

📊 Monitoring Octane

# Check Octane status
php artisan octane:status

# Output example:
# ┌─────────────────────────────────────────┐
# │ Port: 8000                              │
# │ Server: Swoole                          │
# │ Workers: 4                              │
# │ Task Workers: 2                         │
# │ State: Running                          │
# │ Requests: 125,342                       │
# └─────────────────────────────────────────┘

# Memory usage per worker
php artisan octane:status --verbose

# Reload workers (without downtime)
php artisan octane:reload

# Stop Octane
php artisan octane:stop

# Start Octane with custom memory limit
php artisan octane:start --memory-limit=512M
CRITICAL MONITORING

Monitor these metrics in production:

📝 Topic 21 Summary: Laravel Octane

Aspect PHP-FPM Octane (Swoole/RoadRunner)
Bootstrap per request Yes (30-50ms) No (once)
Response time (typical) 50-200ms 5-30ms
Requests per second 50-200 500-2000
Memory leak risk Low (processes restart) High (persistent)
Setup complexity Low (standard) Medium
Code compatibility 100% 95% (some patterns unsafe)
📌 THE RULE: Use Octane when you need maximum performance and can accept the complexity. Test thoroughly for memory leaks. Monitor memory usage in production. Set --max-requests to restart workers periodically. When it works, Octane is magic. When it fails, it fails hard.
NEXT TOPIC PREVIEW

Topic 22: SplFixedArray — The PHP array lie. Why normal arrays are actually hash maps. How SplFixedArray can save 70-80% memory for known-size arrays.