We earn commissions when you shop through the links below.
If you’ve been running a high-traffic Laravel application and hitting a ceiling with traditional PHP-FPM setups, this Laravel Octane performance tuning guide is exactly what you need. Octane supercharges Laravel by keeping the application bootstrapped in memory between requests, eliminating the cold-start penalty you pay with every FPM request. But running Octane naively without tuning it is leaving serious performance on the table.
What Laravel Octane Actually Does
Octane sits in front of your Laravel app using either Swoole or RoadRunner as the underlying server. On startup, it boots the framework once — service providers, container bindings, config — and keeps it all in memory. Subsequent requests reuse that same booted state, which can yield 5–10x throughput improvements over FPM with zero application code changes.
The tradeoff is that shared state across requests can cause subtle bugs if you’re not careful. Tuning Octane means balancing worker count, memory limits, concurrency, and lifecycle hooks to squeeze out maximum performance without introducing state leakage.
Choosing Your Server: Swoole vs RoadRunner
Before you tune anything, you need to pick the right foundation.
- Swoole — A C extension that provides coroutines, async I/O, and higher raw throughput. Best for applications that can take advantage of coroutine-based concurrency. Harder to install in some environments.
- RoadRunner — A Go-based server that’s easier to deploy (single binary) and works well in Docker-heavy setups. Slightly lower peak throughput than Swoole in most benchmarks, but much simpler operationally.
For most production deployments on cloud infrastructure, I lean toward RoadRunner unless you specifically need Swoole’s coroutine features. If you’re deploying to DigitalOcean Droplets or App Platform, RoadRunner’s single binary makes container images cleaner and CI pipelines faster.
Configuring Worker Count
The most impactful tuning lever is how many workers Octane spawns. Each worker handles one request at a time (unless you’re using Swoole coroutines). The general formula is:
# Workers = (Available RAM - OS overhead) / Memory per worker
# On a 4GB server with ~500MB OS overhead:
# (3500MB) / ~100MB per worker ≈ 35 workers max
# But don't max out RAM — leave headroom for spikes
# Practical starting point: (CPU cores * 2) to (CPU cores * 4)
In your config/octane.php:
'swoole' => [
'options' => [
'worker_num' => (int) shell_exec('nproc') * 2,
'task_worker_num' => 6,
'max_request' => 500,
'package_max_length' => 20 * 1024 * 1024, // 20MB
],
],
'roadrunner' => [
'options' => [
'num_workers' => 8,
'max_jobs' => 500,
],
],
The max_request / max_jobs setting is critical. It tells Octane to recycle a worker after N requests, which prevents gradual memory growth from becoming a problem. Setting this too low (< 100) wastes the benefit of persistent processes. Setting it too high (> 2000) risks workers accumulating leaked state. I’ve found 500–1000 to be a solid sweet spot.
Hunting Down Memory Leaks
Memory leaks are the most common reason Octane deployments degrade over time. Because the application stays alive across requests, anything you accidentally store in a static property or a singleton lives forever until that worker recycles.
Common culprits:
- Event listeners bound multiple times inside request cycles
- Static caches that grow unbounded
- Third-party packages that weren’t written with long-running processes in mind
Use Octane’s RequestTerminated hook to clean up between requests:
use Laravel\Octane\Facades\Octane;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
Octane::tick('cleanup', function () {
// runs every tick — use for scheduled cleanup
})->seconds(60);
// In a service provider:
use Laravel\Octane\Events\RequestTerminated;
$this->app['events']->listen(RequestTerminated::class, function ($event) {
// Reset any static state your code might accumulate
MyStaticCache::flush();
});
To actively monitor memory per worker, add logging around Octane’s request lifecycle:
use Laravel\Octane\Events\RequestReceived;
use Laravel\Octane\Events\RequestTerminated;
// In AppServiceProvider::boot()
$this->app['events']->listen(RequestReceived::class, function () {
$this->memoryBefore = memory_get_usage(true);
});
$this->app['events']->listen(RequestTerminated::class, function () {
$diff = memory_get_usage(true) - $this->memoryBefore;
if ($diff > 1024 * 1024) { // 1MB growth per request
logger()->warning('Memory leak detected', [
'growth_bytes' => $diff,
'current_mb' => memory_get_usage(true) / 1024 / 1024,
]);
}
});
Leveraging Octane’s Concurrency Helpers
One underused feature in Octane is concurrent task execution with Swoole. If you have independent operations that can run in parallel, you can dramatically reduce response time:
use Laravel\Octane\Facades\Octane;
[$users, $products, $stats] = Octane::concurrently([
fn () => User::active()->count(),
fn () => Product::featured()->get(),
fn () => DashboardStats::today(),
]);
This spawns three Swoole tasks that execute simultaneously rather than sequentially. On a dashboard endpoint that was previously making three sequential DB queries adding up to 90ms, I’ve seen this drop to ~35ms — roughly the time of the slowest single query.
Note this only works with Swoole. RoadRunner doesn’t support concurrent tasks via Octane’s API.
Caching Strategy for Persistent Workers
Because workers are long-lived, you can safely warm application-level caches once at boot and serve them across many requests. Bind expensive-to-compute objects as singletons in a service provider:
// In a service provider
public function register(): void
{
$this->app->singleton(CountryRepository::class, function () {
// This only runs once per worker boot, not per request
return new CountryRepository(
Cache::remember('countries', 3600, fn () => Country::all())
);
});
}
Be careful here — only use this pattern for data that’s truly read-only or changes infrequently. Mutable state in singletons across requests is how you get subtle, hard-to-reproduce bugs.
Infrastructure and Deployment Tips
Octane rewards you more when the underlying infrastructure is tuned to match. A few things I always check:
- Use a process supervisor — systemd or Supervisor should restart Octane workers if they crash. Never run bare
php artisan octane:startin production without something watching it. - Put a reverse proxy in front — Nginx or Caddy should handle TLS termination, static files, and connection buffering. Don’t expose Octane directly.
- Set memory limits explicitly — Give PHP a hard ceiling with
memory_limit = 256M(or higher for large apps) so runaway workers get killed cleanly rather than causing OOM panics. - Monitor worker recycling — If workers are recycling far before hitting
max_request, you have a memory leak. Use Prometheus + Grafana or New Relic to track per-worker memory over time.
If you’re looking for a simpler deployment target that handles process management for you, Railway supports Docker-based deployments and will restart your Octane container automatically on crashes, which removes a lot of the ops burden.
Quick Wins Checklist
Before you go deep on profiling, run through these quick checks — they’re the highest-ROI items in any Laravel Octane performance tuning guide:
- Enable OPcache with
opcache.enable=1,opcache.memory_consumption=256, andopcache.validate_timestamps=0in production - Run
php artisan config:cache,route:cache, andview:cachein your deployment script - Set
APP_DEBUG=false— debug mode significantly increases response payload size and logging overhead - Use Redis for sessions and cache, not the database driver
- Enable Octane’s built-in table feature for shared in-memory storage if workers need to share counters or flags
Benchmarking Your Changes
Never tune blind. Use wrk or k6 to benchmark before and after each change:
# Install wrk and run a 30-second benchmark with 100 concurrent connections
wrk -t4 -c100 -d30s --latency https://your-app.com/api/endpoint
# Example output to compare:
# Requests/sec: 4823.12
# Latency avg: 20.71ms
# Latency 99%: 89.43ms
Capture p50, p95, and p99 latency alongside throughput. A tuning change that improves average latency but blows up p99 is often worse for real user experience than the baseline.
Final Thoughts
This Laravel Octane performance tuning guide covers the areas where most teams see the biggest gains: worker count, memory management, concurrency, and infrastructure setup. Octane is genuinely one of the most impactful performance improvements available in the Laravel ecosystem, but it requires thoughtful configuration to shine in production. Start with the quick wins, measure everything, and go deeper only where benchmarks tell you there’s still headroom to gain.
If you want to go further with advanced PHP and Laravel architecture patterns, Udemy has solid courses on Laravel internals that complement what you’ve learned here and help you understand the framework deeply enough to tune it with confidence.