Backend development

Laravel Octane and Swoole in 2026: How to Dramatically Speed Up Your PHP Application Without Changing Your Stack

Ruslan Ismailov Published 11 min read
L

Introduction: Why Classic PHP-FPM Slows You Down

Most Laravel applications still run on the Nginx + PHP-FPM stack. It's a reliable, battle-tested setup — but it has a fundamental limitation: every HTTP request triggers a full PHP lifecycle. The interpreter loads files, initializes the framework, boots the service container, handles routing, returns a response — and then destroys everything it created. The next request starts the whole process from scratch.

In practice, this means a significant portion of CPU time and memory is spent not on business logic, but on framework bootstrapping. For high-traffic applications handling thousands of requests per second, this becomes a bottleneck that simply scaling hardware can't fix.

Laravel Octane takes a fundamentally different approach: the application loads once and stays in memory, serving requests without re-initialization. By 2026, this approach has evolved from an exotic technique into a standard practice for production environments with serious performance requirements.

How Laravel Octane Works: Resident Process and Request Lifecycle

Laravel Octane is an official package from the Laravel team that runs your application as a long-lived process on top of a high-performance server — either Swoole or RoadRunner. The key difference from PHP-FPM is that the framework starts only once when the worker boots.

The lifecycle looks like this:

  1. The worker starts and Laravel loads: service providers are registered, the container is built, and dependencies are bound.
  2. An HTTP request arrives. Octane clones the application state (sandbox) and passes control to the router.
  3. The router executes the controller and returns a response to the client.
  4. Octane resets the sandbox state, but does not unload the framework. The next request is handled by the same worker without re-bootstrapping.

This "clone and reset" (fork-reset) mechanism is the critical area that demands developer attention. Any global state not properly reset between requests will cause data leaks between users. We'll cover this in detail in the pitfalls section.

Swoole vs RoadRunner: Comparing Drivers in 2026

Laravel Octane supports two primary drivers. Choosing between them is one of the first decisions you'll make during setup.

Swoole

Swoole is a PHP extension written in C that adds asynchronous primitives to PHP: coroutines, channels, timers, TCP/HTTP servers. Swoole has been at the core of Octane since its inception and remains the top performer in synthetic benchmarks.

  • The built-in HTTP server runs without Nginx as a front proxy (though Nginx is still recommended in production).
  • Coroutine support enables non-blocking calls to databases and Redis directly from PHP.
  • Requires installing an extension — slightly more involved in Docker, but handled with a single line in your Dockerfile.

RoadRunner

RoadRunner is a Go-based server that communicates with PHP workers over a binary protocol. It requires no PHP extensions and is installed as a standalone binary. In 2026, RoadRunner v3 has become significantly more stable and gained native support for gRPC, Temporal workers, and WebSocket.

  • Easier to set up and debug in non-standard environments.
  • Better integration with Go tooling ecosystems (Prometheus metrics, tracing).
  • Slightly lower RPS than Swoole on raw HTTP, but the gap narrows on real applications due to I/O operations.

2026 Recommendation: If your team works in a pure PHP stack and needs maximum HTTP performance — go with Swoole. If extensibility, non-standard protocols, or an existing Go infrastructure matter more — choose RoadRunner.

Installing and Configuring Laravel Octane with Swoole in Docker

Below is a step-by-step guide that works with Laravel 11+ and PHP 8.3.

Dockerfile

FROM php:8.3-cli-alpine\n\nRUN apk add --no-cache \\\n    linux-headers \\\n    $PHPIZE_DEPS \\\n    && pecl install swoole \\\n    && docker-php-ext-enable swoole \\\n    && apk del $PHPIZE_DEPS\n\nRUN docker-php-ext-install pdo pdo_mysql opcache\n\nCOPY --from=composer:2 /usr/bin/composer /usr/bin/composer\n\nWORKDIR /var/www\nCOPY . .\n\nRUN composer install --no-dev --optimize-autoloader\n\nEXPOSE 8000\nCMD ["php", "artisan", "octane:start", "--server=swoole", "--host=0.0.0.0", "--port=8000"]

docker-compose.yml

version: "3.9"\n\nservices:\n  app:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - "8000:8000"\n    environment:\n      APP_ENV: production\n      APP_KEY: "${APP_KEY}"\n      DB_HOST: db\n      REDIS_HOST: redis\n      OCTANE_SERVER: swoole\n      OCTANE_WORKERS: 4\n      OCTANE_MAX_REQUESTS: 500\n    depends_on:\n      - db\n      - redis\n    restart: unless-stopped\n\n  db:\n    image: mysql:8.0\n    environment:\n      MYSQL_DATABASE: laravel\n      MYSQL_ROOT_PASSWORD: secret\n    volumes:\n      - db_data:/var/lib/mysql\n\n  redis:\n    image: redis:7-alpine\n    command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru\n    volumes:\n      - redis_data:/data\n\nvolumes:\n  db_data:\n  redis_data:

config/octane.php Configuration (Key Parameters)

return [\n    'server' => env('OCTANE_SERVER', 'swoole'),\n    'workers' => env('OCTANE_WORKERS', 4),\n    'task_workers' => env('OCTANE_TASK_WORKERS', 2),\n    'max_requests' => env('OCTANE_MAX_REQUESTS', 500),\n    'listeners' => [\n        // Reset singletons between requests\n        RequestReceived::class => [\n            EnsureUploadedFilesAreValid::class,\n        ],\n    ],\n    'warm' => [\n        // Classes to warm up on startup\n        ...Octane::defaultServicesToWarm(),\n    ],\n];

The max_requests parameter defines how many requests a worker handles before restarting. This acts as a safety net against slow memory leaks: even if you have a minor leak, the worker won't run indefinitely and accumulate issues.

Common Pitfalls: Memory Leaks, Static State, and Singletons

Switching to Octane isn't just a matter of changing your start command. A resident process changes the rules, and what was invisible in PHP-FPM can become a critical bug in Octane.

Static Properties

Static PHP properties live for the entire lifetime of the process. If your code has static $cache = [] that gets written to without being reset, data from one user's request will "leak" into another's.

// BAD — data accumulates between requests\nclass UserRepository\n{\n    private static array $cache = [];\n\n    public static function find(int $id): User\n    {\n        return static::$cache[$id] ??= User::find($id);\n    }\n}\n\n// GOOD — use Redis or a request-scoped cache\npublic function find(int $id): User\n{\n    return Cache::store('redis')->remember("user:{$id}", 60, fn() => User::find($id));\n}

Singletons in the Service Container

If you've registered a class as a singleton via app()->singleton() and that class holds state, the state will persist between requests. Octane provides a RequestReceived hook where you can reset such objects:

// In AppServiceProvider\npublic function boot(): void\n{\n    Octane::listen(RequestReceived::class, function () {\n        app()->forgetInstance(MyStatefulService::class);\n    });\n}

Database Connections

Laravel reuses database connections between requests. This is great for performance, but if a transaction was never committed or a connection hangs, the next request inherits a "dirty" state. Use DB::reconnect() in error handlers and keep an eye on mysql_wait_timeout.

Redis Integration for Session Caching and Queues

Redis paired with Laravel Octane is the standard production configuration. Let's look at three key scenarios.

Sessions

# .env\nSESSION_DRIVER=redis\nSESSION_LIFETIME=120\nREDIS_HOST=redis\nREDIS_PORT=6379

Store sessions in Redis, not files — with multiple workers, file-based sessions won't be synchronized across processes.

Cache

CACHE_DRIVER=redis

Octane warms the cache driver when the worker starts. Redis connections are reused between requests, providing an additional latency benefit.

Queues

Run your queue worker as a separate container — not in the same process as Octane. This isolates queue workers from HTTP traffic and simplifies scaling.

  queue:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    command: php artisan queue:work redis --sleep=3 --tries=3\n    depends_on:\n      - redis\n    restart: unless-stopped

Benchmarks: RPS Before and After Octane on a Real Application

Here are load test results from a real Laravel application (CRUD API, JWT authentication, MySQL queries, Redis cache). Testing was performed using wrk on a server with 4 vCPU / 8 GB RAM.

  • PHP-FPM (Nginx + PHP 8.3): ~420 RPS, average response time 240 ms at 100 concurrent connections.
  • Laravel Octane + Swoole (4 workers): ~1850 RPS, average response time 54 ms under the same conditions.
  • Laravel Octane + RoadRunner (4 workers): ~1540 RPS, average response time 65 ms.

A 4–4.5x performance improvement with Swoole and 3.5x with RoadRunner is a typical result for applications with moderate business logic. Applications with heavy SQL queries see smaller gains, since the database becomes the bottleneck rather than PHP bootstrapping.

Important: benchmarks that don't reflect real usage patterns (authentication, database operations, caching) produce inflated numbers. Always test with representative traffic.

Production Deployment Recommendations

Moving Octane to production requires a few extra steps compared to a standard Laravel deployment.

Nginx as a Reverse Proxy

Don't expose the Swoole server directly to the internet. Nginx should handle TLS termination, serve static assets, and proxy dynamic requests to Octane:

server {\n    listen 443 ssl;\n    server_name example.com;\n\n    location /storage {\n        root /var/www/public;\n    }\n\n    location / {\n        proxy_pass http://app:8000;\n        proxy_http_version 1.1;\n        proxy_set_header Connection "";\n        proxy_set_header Host $host;\n        proxy_set_header X-Real-IP $remote_addr;\n    }\n}

Graceful Reload

Don't forcefully kill workers during deployment. Octane supports php artisan octane:reload, which signals workers to finish their current request and restart with the new code. In a Docker environment, this can be implemented via health checks and rolling updates in Kubernetes or Docker Swarm.

Monitoring

  • Track memory usage for each worker — a sudden spike indicates a leak.
  • Export metrics via Laravel Telescope or Prometheus + php-fpm-exporter (a dedicated exporter is available for Swoole).
  • Set up alerts for the 95th percentile latency and worker restart counts.

Pre-Production Checklist

  1. Audit all singletons for state that isn't reset between requests.
  2. Move sessions, cache, and queues to Redis.
  3. Set max_requests to a reasonable value (300–1000 depending on your application).
  4. Configure Nginx as a reverse proxy with keep-alive support.
  5. Run load tests on staging with a realistic traffic profile.
  6. Set up graceful reload in your deployment pipeline.
  7. Add memory and latency monitoring.

Conclusion

Laravel Octane with Swoole is not a silver bullet, but it is one of the most accessible ways to dramatically boost PHP application performance without changing your stack or rewriting your business logic. By 2026, the tool is mature, well-documented, and proven in production at scale by large engineering teams.

The most important step before migrating is auditing your code for state that isn't meant to be shared between requests. Once that's handled, setup takes a few hours — and the performance gains pay off many times over.

Technologies

Tags

Ruslan Ismailov

Senior Web / Backend Developer. Senior web/backend developer with 9 years of experience. Stack: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, microservices, CI/CD. More about me →