
How to Make Laravel Faster: Six Fixes in the Order That Actually Matters
Mahmud Hasan
October 10, 2026
Your Laravel app feels slow, and the standard advice is a checklist with no order: cache this, index that, enable Octane, buy Redis. The problem with checklists is they hide the one question that matters — where is the time actually going? In most Laravel apps, the answer is embarrassing: a large chunk of every request is spent booting the framework itself. Config files read, routes registered, views compiled, PHP recompiled — all for one request, then thrown away. So before you touch a query, fix the boot. In that order, six things, from free to serious.
Step 0: measure, or you'll optimize the wrong thing
Every performance story that ends badly starts the same way: someone assumed the database was slow. Install Laravel Debugbar on staging (never production) and look at three numbers: total queries per page, how many are duplicates, and how much of the response time is spent before your first query even runs. If you're seeing 200 queries on a page that renders a list of 50 items, that's the N+1 problem, and no amount of server tuning fixes it. If the framework takes 150ms before your controller code starts, that's the boot tax, and no query optimization fixes that either. Ten minutes of measuring beats a week of guessing, because these two diseases have opposite cures.
Step 1: the 30-second win — cache your deploy
Laravel ships a single command that most tutorials bury in a deployment checklist: php artisan optimize. It caches your configuration, event listeners, routes, and compiled views in one shot. The granular versions are config:cache, event:cache, route:cache, and view:cache — the official deployment docs say config caching "greatly reduces the number of trips the framework must make to the filesystem," and route caching collapses hundreds of route registrations into a single method call.
This belongs in your deploy script, not your memory, because it has one famous footgun: after config:cache, the .env file is never loaded again. Any env() call outside your config files returns null. If your app reads env('STRIPE_KEY') in a controller, caching config will silently break payments in production. The rule is simple: everything reads config via config(), env() only inside config files. Get that right and this step is free, instant, and permanent. Also add composer install --optimize-autoloader --no-dev to the deploy — dev dependencies and a lazy autoloader don't belong in production.
Step 2: OPcache, the free doubling nobody verifies
OPcache stores PHP's compiled bytecode in shared memory so the engine doesn't recompile your files on every request. It's the single highest-ROI line of config in PHP, and it ships with PHP — you just have to turn it on and size it:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
That last line is the one people get wrong. validate_timestamps=0 tells OPcache to never check whether files changed — a big win under load, but it means deploys won't take effect until you restart PHP-FPM. That's the correct trade-off in production; in staging, leave it on so your edits appear. And here's the part almost nobody does: verify it. Call opcache_get_status() and check the hit rate — it should be ≥99%, with zero out-of-memory restarts. An OPcache that's constantly evicting and recompiling is a recurring cold start wearing a performance costume. One more thing: skip the JIT hype for web apps. PHP 8.4 leaves opcache.jit disabled by default, and enabling it only pays off for CPU-bound work — which your request lifecycle almost certainly isn't.
Step 3: your queries are probably fine — your N+1 isn't
Now, and only now, look at the database. The classic Laravel disease is the N+1: one query for the list, then one query per item for a relationship. The fix is one word: with().
// 1 + 50 queries
$users = User::all();
// 2 queries
$users = User::with('posts')->select('id', 'name')->get();
Two habits matter beyond eager loading. First, stop fetching columns you don't render — select() the fields you need instead of dragging full rows through memory. Second, process large datasets in chunks with chunkById(1000, ...) instead of loading ten thousand models into memory at once. Then check your indexes: if a column appears in a where or join on a hot path and has no index, the database is scanning rows it could have jumped to. A word about the lazy answer: throwing Redis at an N+1 problem doesn't fix it, it just caches the waste. Fix the queries, then cache what's left.
Step 4: get work off the request
Some work doesn't need to happen while the user waits. Sending mail, processing images, calling slow third-party APIs, generating PDFs — if it's not required for the response, it shouldn't be in the request. Laravel queues exist for exactly this: dispatch the job, return the response, let a worker finish in the background (php artisan queue:work redis). Move cache, sessions, and queues to Redis while you're at it — file-based sessions under concurrent load are a lock-contention trap. The rule of thumb: if a user action triggers more than ~200ms of non-essential work, that's a job, not a controller.
Step 5: Octane — the 3x lever with fine print
Everything so far reduces the cost of booting. Octane eliminates the boot. It's Laravel's package for running your app on long-lived application servers — FrankenPHP, RoadRunner, or Swoole — where the framework boots once and stays in memory, serving request after request without re-initializing. Install is two commands: composer require laravel/octane then php artisan octane:install. The official docs recommend serving it behind nginx (static assets + SSL termination there, proxy to Octane on port 8000) with a process monitor like Supervisor keeping it alive.
The numbers are real. A Deploynix benchmark from April 2026 ran a representative Laravel app — Eloquent queries, cache reads, Blade rendering, not a hello-world route — on a Hetzner box with PHP 8.4: PHP-FPM managed 850 requests per second, Octane with RoadRunner hit 2,350 (2.8x), and Octane with Swoole hit 2,600 (3.1x). Median latency dropped from 45ms to 14–18ms, and time-to-first-byte on cached pages went from 25ms to 3–5ms — a 5–8x improvement. Memory is the trade: Octane workers hold the whole bootstrapped app, 55–100MB each versus 30–50MB for PHP-FPM workers. But four Octane workers outperformed ten PHP-FPM workers at similar total memory. The fine print matters though. Because the app stays alive between requests, state leaks: never inject the request, config, or container into a singleton — use scoped() bindings for per-request services — and recycle workers with --max-requests. Swoole's Octane::concurrently() is capped at 1024 tasks; the runtime-agnostic Concurrency::run() is the safer default. And Octane isn't for everyone: shared hosting often can't run it, tiny apps don't need the operational complexity, and any deploy must restart Octane or workers keep serving old code (Laravel's php artisan reload exists for exactly this). Octane is the biggest lever on this list, which is why it's last — if steps 1–4 aren't done, you're just booting faster into the same problems.
The order, on one sticky note
Measure with Debugbar first. Then, in this order: php artisan optimize + composer install --optimize-autoloader --no-dev in your deploy script; OPcache on, sized, timestamp validation off in prod, hit rate verified ≥99%; kill the N+1s, select only the columns you render, chunk big datasets, index hot where columns; move non-essential work to queues on Redis; and only then consider Octane — with worker recycling and scoped bindings, behind nginx. Most apps I see get 80% of the available speedup from the first three steps, which cost nothing and take an afternoon. The remaining 20% is Octane, and now you know exactly what it costs. That's the whole secret: speed isn't a feature you buy, it's waste you remove, in order.
References
- Laravel 12.x Deployment docs — optimization commands, env() gotcha, APP_DEBUG warning, reload command
- Laravel 12.x Octane docs — drivers, install, nginx proxy config, Supervisor setup, DI/memory-leak guidance
- Deploynix: PHP-FPM vs Laravel Octane benchmark (April 2026) — 850 vs 2,100–2,600 RPS, latency percentiles, memory figures
- Kamran Khalid: Laravel Performance Boosters — config/route caching, OPcache values, eager loading, queues
Comments
More in Web Development

The AI Boom's Favorite Number Just Dropped From $70B to $50B. Wall Street Noticed.
The FT says OpenAI's annualized revenue is $50 billion, not the $70 billion investors were using. Wall Street sold tech on the difference — here's why one disputed number can sink trillions, and what to watch instead of model launches.
Read more
