Laravel Octane Performance: Production Results & Implementation Guide - NextGenBeing Laravel Octane Performance: Production Results & Implementation Guide - NextGenBeing
Back to discoveries

Laravel Octane: Real-World Performance Gains Without Changing Your Infrastructure

Discover how we achieved 5x request throughput and 60% latency reduction using Laravel Octane in production—without touching our server setup or rewriting code.

AI Workflows Premium Content 29 min read
Aaron Vasquez

Aaron Vasquez

May 25, 2026 0 views
Size:
Height:
📖 29 min read 📝 9,163 words 👁 Focus mode: ✨ Eye care:

Listen to Article

Loading...
0:00 / 0:00
0:00 0:00
Low High
0% 100%
⏸ Paused ▶️ Now playing... Ready to play ✓ Finished

Last March, our SaaS platform hit a wall. We were serving around 500 requests per second during peak hours, and our AWS bill was climbing faster than our revenue. Our CTO, Maria, gave us a challenge: "Double our capacity without spinning up more EC2 instances." I thought she was joking. She wasn't.

We'd already optimized our database queries, implemented Redis caching, and fine-tuned our Nginx configuration. The low-hanging fruit was gone. That's when I started digging into Laravel Octane—Laravel's official application server that keeps your application loaded in memory between requests. I was skeptical. The promise sounded too good: "10x performance improvements" without rewriting your codebase or changing your infrastructure.

Spoiler: We didn't get 10x. But we did get 5x request throughput and reduced our P95 latency from 280ms to 110ms. More importantly, we did it without touching our infrastructure setup, without rewriting application code, and without introducing the architectural complexity I was dreading.

Here's everything I learned deploying Laravel Octane to production, including the three major gotchas that almost derailed us, the performance characteristics nobody talks about, and the specific scenarios where Octane actually hurts more than it helps.

Why Traditional PHP-FPM Is Your Performance Bottleneck

Before we dive into Octane, let's talk about why standard Laravel applications are slow by design. This isn't Laravel's fault—it's PHP's process model.

With PHP-FPM (the default for most Laravel deployments), every single HTTP request follows this pattern:

  1. Nginx receives the request and forwards it to PHP-FPM
  2. PHP-FPM spawns or reuses a worker process
  3. The worker loads your entire Laravel application from scratch
  4. It parses your routes, loads service providers, boots the framework
  5. Your controller executes and generates a response
  6. The worker tears everything down and waits for the next request

Steps 3-5 happen every single time. Even if you're serving the exact same route a millisecond later, Laravel reloads everything. The framework bootstrap alone typically takes 50-80ms on a modest server. For simple CRUD operations that might only take 10-20ms of actual work, you're spending 70-80% of your response time just loading the framework.

I profiled our application using Blackfire last year, and the numbers were embarrassing. On our /api/projects endpoint—a simple paginated list with some eager loading—here's what I found:

Total response time: 145ms
├─ Framework bootstrap: 68ms (47%)
├─ Service provider registration: 22ms (15%)
├─ Route matching: 8ms (6%)
├─ Database queries: 31ms (21%)
└─ Response rendering: 16ms (11%)

Nearly half our response time was Laravel just waking up. We weren't doing anything wrong—this is just how PHP-FPM works. Every request pays the bootstrap tax.

How Octane Changes the Game

Laravel Octane flips this model on its head. Instead of loading your application for every request, Octane loads it once and keeps it in memory. When requests come in, they're routed directly to your already-booted application. No bootstrap, no service provider registration, no route recompilation.

Under the hood, Octane uses one of two application servers: Swoole or RoadRunner. Both are long-running PHP processes that maintain your application state between requests. Here's what the request lifecycle looks like with Octane:

  1. Nginx receives the request and forwards it to Octane
  2. Octane's already-running worker receives the request
  3. Your controller executes (Laravel is already booted)
  4. Response is returned
  5. Worker resets request-specific state and waits for the next request

Notice what's missing? Steps 3-4 from the PHP-FPM model. The framework bootstrap happens once when Octane starts, not on every request.

When I first deployed this to our staging environment, I ran the same /api/projects profiling:

Total response time: 58ms
├─ Framework bootstrap: 0ms (already booted)
├─ Service provider registration: 0ms (already booted)
├─ Route matching: 3ms (5%)
├─ Database queries: 38ms (66%)
└─ Response rendering: 17ms (29%)

The 68ms bootstrap disappeared entirely. Our response time dropped from 145ms to 58ms—a 60% reduction—without changing a single line of application code.

Swoole vs RoadRunner: Which One Should You Choose?

Laravel Octane supports two application servers, and choosing between them matters more than the documentation suggests. I've run both in production, and they have different characteristics that aren't obvious until you hit scale.

Swoole: The Performance Champion

Swoole is a PHP extension written in C that turns PHP into an asynchronous, coroutine-based runtime. It's blazingly fast but requires compiling a PHP extension, which complicates deployment.

When I use Swoole:

  • High-traffic APIs where every millisecond matters
  • When I control the infrastructure (Docker images, EC2 instances)
  • Applications that can benefit from Swoole's async features (concurrent HTTP calls, WebSockets)

Installation gotcha I hit: Swoole requires Short Open Tags to be disabled in your php.ini. I spent two hours debugging why Octane wouldn't start until I found this buried in a GitHub issue. Add this to your php.ini:

short_open_tag = Off

Our production Swoole setup handles about 2,800 requests per second on a c5.xlarge instance (4 vCPUs, 8GB RAM). Here's our supervisor configuration:

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

The --max-requests=1000 flag is critical. It tells Swoole to restart workers after 1,000 requests, which prevents memory leaks from accumulating. Without this, we saw memory usage creep from 80MB per worker to over 400MB after a few hours.

RoadRunner: The Deployment-Friendly Option

RoadRunner is a standalone Go binary that manages PHP worker processes. It's slightly slower than Swoole but much easier to deploy—no PHP extension compilation required.

When I use RoadRunner:

  • Shared hosting or platforms where I can't compile extensions
  • Teams that aren't comfortable with Swoole's complexity
  • Applications that don't need Swoole's async features

Performance difference in our tests: On the same hardware, RoadRunner handled about 2,200 requests per second vs Swoole's 2,800. That's a 21% difference, but for many applications, RoadRunner's ease of deployment is worth the trade-off.

Here's our RoadRunner configuration (.rr.yaml):

server:
  command: "php artisan octane:start --server=roadrunner"

http:
  address: 127.0.0.1:8000
  max_request_size: 10
  middleware: []
  pool:
    num_workers: 8
    max_jobs: 1000
    allocate_timeout: 60s
    destroy_timeout: 60s

logs:
  mode: production
  level: error
  output: stderr

The max_jobs: 1000 setting is RoadRunner's equivalent to Swoole's --max-requests. Same purpose: prevent memory leaks from spiraling.

My recommendation: Start with RoadRunner. If you hit performance bottlenecks and need that extra 20-30% throughput, migrate to Swoole. Don't optimize prematurely—RoadRunner is fast enough for 95% of applications.

The Three Gotchas That Almost Killed Our Deployment

When I first deployed Octane to production, I thought I'd done my homework. I'd read the docs, tested in staging, and felt confident. Then Friday afternoon happened, and everything broke in ways I didn't expect.

Gotcha #1: Memory Leaks From Singleton Services

This was the big one. After about 30 minutes in production, our Octane workers were consuming 600MB of memory each. Within two hours, we were getting OOM kills. I panicked and rolled back.

The problem? We had several services registered as singletons in our service container that were accumulating state between requests. Here's the specific service that killed us:

// In AppServiceProvider
public function register()
{
    $this->app->singleton(ActivityLogger::class, function ($app) {
        return new ActivityLogger();
    });
}

Our ActivityLogger was designed to collect activity events during a request and bulk-insert them at the end. In PHP-FPM, this worked fine because the singleton was destroyed after each request. In Octane, the singleton persisted, and we were appending to the same array for every request.

After 10,000 requests, that array had 10,000 entries, each holding references to Eloquent models. Memory usage exploded.

The fix: Make stateful services request-scoped instead of singletons. Laravel provides the scoped binding for this:

public function register()
{
    $this->app->scoped(ActivityLogger::class, function ($app) {
        return new ActivityLogger();
    });
}

Scoped bindings are created once per request and destroyed when the request completes. Problem solved.

How to find these issues before production: I wrote a simple artisan command to stress-test our Octane workers and monitor memory:

// app/Console/Commands/OctaneMemoryTest.php
namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Http;

class OctaneMemoryTest extends Command
{
    protected $signature = 'octane:memory-test {url} {--requests=1000}';
    
    public function handle()
    {
        $url = $this->argument('url');
        $requests = $this->option('requests');
        
        $this->info("Sending {$requests} requests to {$url}...");
        
        for ($i = 0; $i < $requests; $i++) {
            Http::get($url);
            
            if ($i % 100 === 0) {
                $this->line("Completed {$i} requests");
            }
        }
        
        $this->info("Check Octane worker memory usage now.");
    }
}

Run this against your local Octane server and monitor memory with ps aux | grep octane. If memory keeps climbing, you've got a leak.

Gotcha #2: Database Connection Pooling Issues

Our second production incident happened at 2am on a Tuesday. Our monitoring showed database connection errors spiking. The error messages were confusing:

SQLSTATE[HY000] [2002] Connection timed out
SQLSTATE[HY000] [2006] MySQL server has gone away

These errors usually mean your database is overwhelmed, but our RDS metrics showed CPU at 30% and connections well below our limit. What was happening?

The issue: Octane workers maintain persistent database connections. In PHP-FPM, connections are opened and closed with each request. In Octane, connections are opened once and reused. If a connection dies (network blip, idle timeout, database restart), Octane doesn't automatically reconnect.

We were hitting MySQL's wait_timeout setting (8 hours by default). When a worker's connection sat idle for 8 hours overnight, MySQL killed it. The next request on that worker failed.

The fix: Enable automatic reconnection in your database configuration:

// config/database.php
'mysql' => [
    'driver' => 'mysql',
    'host' => env('DB_HOST', '127.0.0.1'),
    'port' => env('DB_PORT', '3306'),
    'database' => env('DB_DATABASE', 'forge'),
    'username' => env('DB_USERNAME', 'forge'),
    'password' => env('DB_PASSWORD', ''),
    'charset' => 'utf8mb4',
    'collation' => 'utf8mb4_unicode_ci',
    'prefix' => '',
    'strict' => true,
    'engine' => null,
    'options' => [
        PDO::ATTR_PERSISTENT => false,
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    ],
    // These two settings are critical for Octane
    'sticky' => true,
    'reconnect' => true,
],

The reconnect => true option tells Laravel to automatically reconnect if a connection dies. The sticky => true option ensures read/write connections are maintained properly in a load-balanced setup.

Additional gotcha: If you're using database transactions, be extra careful. A transaction that's never committed or rolled back will persist across requests in Octane. Always use try-finally blocks:

DB::beginTransaction();
try {
    // Your transactional code
    DB::commit();
} catch (\Exception $e) {
    DB::rollBack();
    throw $e;
}

Or better yet, use Laravel's transaction helper:

DB::transaction(function () {
    // Your code - automatically committed or rolled back
});

Gotcha #3: Session Data Corruption

This one was subtle and took us a week to track down. Users were randomly seeing other users' data. Not frequently—maybe once every few thousand requests—but frequently enough to be terrifying.

The root cause: We had a middleware that was storing data in the session, but we weren't properly isolating session state between requests. Here's the problematic code:

class TrackUserActivity
{
    public function handle($request, Closure $next)
    {
        session(['last_activity' => now()]);
        session(['current_page' => $request->path()]);
        
        return $next($request);
    }
}

In PHP-FPM, this works fine. Each request gets its own session instance. In Octane, if two requests hit the same worker in rapid succession, they could share session state if the session wasn't properly reset between requests.

Unlock Premium Content

You've read 30% of this article

What's in the full article

  • Complete step-by-step implementation guide
  • Working code examples you can copy-paste
  • Advanced techniques and pro tips
  • Common mistakes to avoid
  • Real-world examples and metrics

Join 10,000+ developers who love our premium content

Aaron Vasquez

Aaron Vasquez

Author

Covers DevOps practices, CI/CD pipelines, Kubernetes, and platform engineering. Contributing author at NextGenBeing.

Never Miss an Article

Get our best content delivered to your inbox weekly. No spam, unsubscribe anytime.

Comments (0)

Please log in to leave a comment.

Log In

Related Articles

Don't miss the next deep dive

Get one well-researched tutorial in your inbox each week. No spam, unsubscribe anytime.