
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
A Laravel app feels fast on your laptop with two rows in the database, then crawls the moment real traffic and a real dataset arrive. Most of the slowdown is not the framework — it is uncached config, N+1 queries, missing indexes, and slow work running inside the request instead of a queue. This guide walks through 15 Laravel performance optimization techniques that actually move the numbers on Laravel 10/11 and PHP 8.4, each with the reason it works and the exact code to apply it. If you would rather have this audited for you, see the Laravel performance and DevOps services.
php artisan optimize, enabling OPcache, fixing N+1 queries through eager loading, adding database indexes, moving slow work to queues, and caching hot queries in Redis. Together these routinely cut response times by more than half.What is the fastest way to speed up a Laravel application?
The single fastest change is caching what Laravel otherwise rebuilds on every request. In production, config, routes, and compiled views should never be parsed from source on each hit. One command bakes them all:
php artisan optimize
# runs config:cache, route:cache, view:cache and event:cache together
# clear them again during a deploy before re-caching:
php artisan optimize:clear Never run optimize or config:cache in local development — once config is cached, env() calls outside config files return null. Cache in production only, as the final step of a deploy, which pairs naturally with an automated pipeline like the one in this GitLab CI/CD pipeline for Laravel walkthrough.
The 15 techniques at a glance
- Cache config, routes, views and events with
artisan optimize. - Enable and tune OPcache for the PHP-FPM runtime.
- Add OPcache preloading for the framework's hot classes.
- Fix N+1 queries with eager loading (
with()). - Select only the columns you need, and paginate large sets.
- Add database indexes to filtered and joined columns.
- Chunk or lazily stream large query results.
- Cache expensive query results in Redis.
- Move sessions and the cache store to Redis.
- Offload slow work (email, exports, APIs) to queue workers.
- Cache whole responses for anonymous, read-heavy pages.
- Bundle and minify front-end assets with Vite.
- Serve static assets and images through a CDN.
- Run Laravel Octane for a persistent, booted application.
- Measure with Debugbar, Telescope and query counts before and after.
How do you fix slow database queries in Laravel?
Slow queries, not PHP, are the usual culprit behind a sluggish page. Three problems cause most of it: N+1 queries, over-fetching columns, and missing indexes.
Technique 4 — eliminate N+1 queries with eager loading
An N+1 query happens when you load a list, then trigger one extra query per row to fetch a relationship. Fifty posts with a lazy author access become 51 queries. Eager loading collapses that to two:
# N+1: 1 query for posts + 1 per post for the author
$posts = Post::all();
foreach ($posts as $post) {
echo $post->author->name; # fires a query each iteration
}
# Fixed: 2 queries total, regardless of row count
$posts = Post::with('author')->get();
# Guard against it app-wide (Laravel 10/11) in a service provider:
Model::preventLazyLoading(! app()->isProduction()); preventLazyLoading throws in local and staging the moment a relationship is accessed without eager loading, so N+1 problems surface in development instead of production.
with() turns one query per row into a single batched WHERE id IN (…) lookup.Technique 5 — select only what you need and paginate
Fetching every column and every row wastes memory and bandwidth. Ask for the columns the view uses, and never return an unbounded list:
# Over-fetching: all columns, all rows
$users = User::all();
# Lean: only needed columns, paginated
$users = User::query()
->select(['id', 'name', 'email'])
->where('active', true)
->paginate(25); Technique 6 — add indexes to filtered and joined columns
Any column used in a WHERE, ORDER BY, or JOIN should be indexed. Without an index, MySQL scans the whole table. Add them in a migration:
Schema::table('posts', function (Blueprint $table) {
$table->index('user_id'); # foreign key lookups
$table->index(['status', 'published_at']); # composite for filtered lists
}); Confirm the index is actually used by prefixing the query with EXPLAIN — you want ref or range in the type column, not ALL (a full scan).
Technique 7 — chunk large result sets
Processing tens of thousands of rows at once exhausts memory. chunkById and lazy() stream them in batches:
# Process 1,000 rows at a time without loading them all
User::where('active', true)->chunkById(1000, function ($users) {
foreach ($users as $user) {
# work on each user
}
});
# Or stream one model at a time via a generator
foreach (User::lazy() as $user) {
# constant memory footprint
} How do caching and Redis improve Laravel performance?
Once queries are lean, the next gains come from not repeating work. Redis is fast in-memory storage that serves as Laravel's cache, session, and queue backend.
Technique 8 — cache expensive query results
Wrap a costly, rarely-changing query in Cache::remember. The closure runs only on a cache miss:
use Illuminate\Support\Facades\Cache;
$stats = Cache::remember('dashboard.stats', now()->addMinutes(10), function () {
return [
'users' => User::count(),
'revenue' => Order::where('paid', true)->sum('total'),
];
}); Technique 9 — put the cache and sessions on Redis
The default file cache and session drivers hit disk and do not scale across servers. Point both at Redis in your .env:
CACHE_STORE=redis
SESSION_DRIVER=redis
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis # the C extension is faster than predis The phpredis PHP extension is noticeably faster than the pure-PHP predis client and is the recommended default on Laravel 10/11.
How do queues and workers speed up Laravel requests?
A web request should return in milliseconds. Anything slow — sending mail, generating a PDF, calling a third-party API, resizing an image — belongs on a queue, so the user gets an instant response while a worker does the heavy lifting in the background.
Technique 10 — offload slow work to a queue
# Instead of sending mail inline (blocks the response):
# Mail::to($user)->send(new WelcomeMail($user));
# Queue it — the request returns immediately:
Mail::to($user)->queue(new WelcomeMail($user));
# Or dispatch a job:
ProcessPodcast::dispatch($podcast)->onQueue('media'); Run a persistent worker in production (kept alive by Supervisor or systemd), not queue:work in a terminal:
php artisan queue:work redis --queue=default,media --tries=3 --max-time=3600 Technique 11 — cache whole responses for read-heavy pages
For anonymous, mostly-static pages (a blog index, a marketing page), caching the full rendered response skips the controller and database entirely on repeat hits. A middleware pattern:
$key = 'page:' . sha1($request->fullUrl());
return Cache::remember($key, now()->addHour(), function () use ($request) {
return response($this->renderPage($request));
}); Which server-level tweaks make Laravel faster?
The runtime and delivery layer matter as much as the code.
Technique 2 & 3 — enable OPcache and preloading
OPcache stores compiled PHP bytecode in memory so scripts are not recompiled on every request — often the biggest single production win. In php.ini:
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; production only — clear OPcache on deploy
opcache.jit=1255 ; PHP 8.4 JIT
opcache.jit_buffer_size=128M With validate_timestamps=0, PHP never checks files for changes, so you must reload PHP-FPM (or clear OPcache) on each deploy. Preloading goes further by loading the framework's hot classes into shared memory once at startup via opcache.preload.
Technique 12 & 13 — bundle assets and serve them from a CDN
Compile and minify CSS/JS for production so browsers download fewer, smaller files, then push static assets to a CDN close to users:
npm run build # Vite: minified, hashed, versioned assets
# .env — rewrite asset URLs to the CDN host
ASSET_URL=https://cdn.example.com Technique 14 — run Laravel Octane for a persistent app
Traditional PHP-FPM boots the framework on every request. Octane keeps a booted application resident in memory using Swoole or FrankenPHP, so each request skips the bootstrap and reuses warm objects — a large throughput gain for API-heavy apps:
composer require laravel/octane
php artisan octane:install --server=frankenphp
php artisan octane:start --workers=4 --max-requests=500 Octane changes the memory model: the container persists between requests, so avoid storing request-specific state in singletons and watch for memory leaks. Test thoroughly before switching a mature app.
How do you measure Laravel performance improvements?
Optimise against numbers, not hunches. Technique 15 is measurement itself.
- Laravel Debugbar — shows query count, timings, and memory per page in local development. If a page fires 60 queries, you have an N+1 problem to hunt.
- Laravel Telescope — records slow queries, jobs, and requests in staging.
- Count queries in a test to lock in a fix and prevent regressions:
use Illuminate\Support\Facades\DB;
DB::enableQueryLog();
$this->get('/dashboard');
$this->assertLessThan(10, count(DB::getQueryLog())); Always record a baseline (response time and query count) before a change and compare after — that is the only way to know a technique actually worked for your workload.
Conclusion
Laravel performance optimization is not one silver bullet; it is a stack of small, measurable wins. Start with the cheapest and highest-impact ones — artisan optimize, OPcache, and fixing N+1 queries — then add indexes, Redis, and queues as your traffic grows, and reach for Octane only once the basics are in place. Measure before and after each change so you know it helped. If you want a data-backed performance audit of your Laravel app, get in touch or browse the performance and DevOps case studies for real-world results.