Laravel performance optimization checklist (2026)

Last updated:

Reading time: 9 min

Laravel performance optimization checklist (2026)

A slow Laravel application is rarely slow for one reason. It is usually a combination of small, fixable issues, a missing index here, an N+1 query there, a cache driver that was never changed from its default. Individually, each item below might save a few milliseconds. Together, they are often the difference between an application that feels instant and one that feels sluggish under real traffic.
This checklist is organized by layer, application code, database, infrastructure, and monitoring. Work through it in order. Each item includes a code example and the concrete impact of applying it.

Application-level optimizations
1. Cache configuration, routes, and views

Laravel reads and parses configuration files, route definitions, and Blade templates on every request unless you tell it not to. In production, none of that should happen more than once per deployment.

php artisan optimize 

This single command caches your configuration, routes, events, and compiled views in one pass. Run it as part of your deployment pipeline, not manually on a live server, and re-run it after every deploy since stale caches will otherwise serve outdated config or routes.

php artisan optimize:clear 

Impact: eliminates the overhead of parsing PHP config files and re-registering hundreds of routes on every single request. For applications with large route files or many service providers, this can measurably cut response time before your application logic even runs.

2. Eliminate N+1 queries with eager loading


This is the single most common performance issue in Laravel applications, and it is easy to introduce without noticing. Lazy loading a relationship inside a loop fires one query per iteration.

 // N+1: fires one query per post to load its author 
$posts = Post::all(); 
foreach ($posts as $post) { 
echo $post->author->name; 
} 
// Fixed: one query for posts, one query for all related authors 
$posts = Post::with('author')->get(); 
foreach ($posts as $post) { 
echo $post->author->name; 
} 

Catch these automatically in development by preventing lazy loading outright:

 // AppServiceProvider::boot() 
Model::preventLazyLoading(! $this->app->isProduction()); 

Impact: a page that fires 50 queries because of an N+1 problem often drops to 2 or 3 queries with eager loading. On database-heavy pages, this is typically the highest-return fix on this entire list, and preventLazyLoading() means your test suite catches regressions before they reach production.

3. Move non-essential work to queues


Anything that does not need to finish before you respond to the user, sending an email, generating a report, calling a third-party API, should not run inside the request-response cycle.

class SendWelcomeEmail implements ShouldQueue 
{ 
 public function handle(User $user): void 
 {
 Mail::to($user)->send(new WelcomeMail($user)); 
 } 
} 
dispatch(new SendWelcomeEmail($user)); 
Use Queue::route(), added in Laravel 13, to control where each job runs from one central location: 
Queue::route(SendWelcomeEmail::class, queue: 'emails', connection: 'redis'); 

Impact: moves slow, non-critical work off the request thread entirely, so a user submitting a form gets an immediate response instead of waiting on an email provider's API. Run a queue worker with Laravel Horizon for visibility into job throughput and failures.

4. Cache expensive queries and computations

If a query or calculation produces the same result for multiple users or requests, compute it once and store it.

$topProducts = Cache::remember('top-products', now()->addHour(), function () { 
return Product::orderBy('sales_count', 'desc')->limit(10)->get(); 
}); 

When the cached value just needs its expiration pushed back rather than a full recompute, use Cache::touch(), added in Laravel 13, instead of reading and rewriting the value:

Cache::touch('top-products', now()-
>addHour()); 

Impact: removes repeated, identical database load for data that does not change every request, like homepage widgets, navigation menus, or dashboard summaries. Cache::touch() specifically avoids an unnecessary read-and-rewrite round trip on cache extensions.

5. Optimize Composer's autoloader for production

Composer's default autoloader resolves classes dynamically. In production, that lookup should be precomputed.

composer install --no-dev --optimize-autoloader 

Impact: generates a class map instead of scanning the filesystem for classes at runtime, and strips out development-only packages like testing and debugging tools that add overhead without providing any production value. Database optimizations

6. Add indexes to frequently queried columns

Any column used in a WHERE, JOIN, or ORDER BY clause on a large table needs an index, and foreign keys should always be indexed.

Schema::table('orders', function (Blueprint $table) {
$table->index('user_id'); 
$table->index(['status', 'created_at']); 
}); 

Impact: on a table with tens of thousands of rows or more, an unindexed lookup forces a full table scan. Adding the right index can take a query from seconds to milliseconds. Use EXPLAIN on your slowest queries to confirm indexes are actually being used before assuming this is solved.

7. Select only the columns you need

Pulling every column from a wide table, especially one with large text or JSON columns, costs bandwidth and memory even when you only need a few fields.

// Pulls every column, including ones you don't need $users = User::all(); 
// Pulls only what the view actually uses $users = User::select(['id', 'name', 'email'])->get(); 

Impact: reduces both the data transferred from the database and the memory Laravel spends hydrating Eloquent models, which matters most on list views and API endpoints returning large collections.

8. Chunk large datasets instead of loading them all at once

Calling all() or get() on a table with millions of rows attempts to load every row into memory simultaneously.

// Loads the entire table into memory at once 
User::all()->each(fn ($user) => $user->sendNewsletter()); 
// Processes 500 rows at a time 
User::chunk(500, function ($users) { 
foreach ($users as $user) { 
$user->sendNewsletter(); 
}
 }); 

Impact: prevents memory exhaustion on large tables and keeps memory usage flat regardless of table size, which is essential for any scheduled job, export, or bulk update that touches more than a few thousand rows.

9. Paginate instead of returning full collections

Any endpoint that could return an unbounded number of rows should be paginated, both for performance and for API consumers.

return Order::where('status', 'completed')->paginate(25); 

Impact: bounds both the query result size and the JSON payload sent to the client, keeping response times consistent regardless of how much data exists in the table.
Infrastructure and server configuration

10. Run PHP 8.3 or newer with OPcache enabled

Laravel 13 requires PHP 8.3 as a minimum, and that requirement is also a performance win, modern PHP includes JIT compilation and general engine improvements over older versions.

; php.ini opcache.enable=1 opcache.enable_cli=0 opcache.memory_consumption=256 opcache.max_accelerated_files=20000 opcache.validate_timestamps=0 

Impact: OPcache stores precompiled PHP bytecode in memory so scripts do not need to be re-parsed and re-compiled on every request. Setting validate_timestamps=0 in production skips the file-modification check on every request, but requires you to manually clear OPcache on each deploy.

11. Use Redis for cache, sessions, and queues

The default file and database drivers for cache and sessions work, but they involve disk I/O or extra database queries on every read and write.

CACHE_STORE=redis SESSION_DRIVER=redis QUEUE_CONNECTION=redis 

Impact: moves cache, session, and queue reads and writes into memory instead of disk or your primary database, which reduces load on the database that serves your actual application data and cuts latency on every cache hit.

12. Serve static assets through a CDN

JavaScript, CSS, images, and fonts do not need to be served from your application server on every request.

ASSET_URL=https://cdn.yourdomain.com 

Impact: offloads static asset delivery to edge servers geographically closer to your users, reducing both load on your application server and time-to-first-byte for anything not generated dynamically.

13. Consider Laravel Octane for high-throughput applications

Standard PHP-FPM bootstraps the entire framework on every single request. Octane, Laravel's application server package, keeps the framework in memory between requests using Swoole or RoadRunner.

composer require laravel/octane php artisan octane:install php artisan octane:start --server=swoole 

Impact: on high-traffic applications, this removes the framework bootstrap cost from every request, which can meaningfully increase requests-per-second. This is worth adopting for high-throughput APIs, it is generally not necessary for typical low-to-medium traffic applications, and it does require auditing your code for state that should not persist between requests.

14. Use Reverb's database driver for lean real-time features

If you are building real-time features, live notifications, dashboards, or chat, Laravel 13 adds a database driver for Reverb, the framework's WebSocket server, so horizontal scaling no longer strictly requires Redis.

// config/broadcasting.php 'reverb' => [ 'driver' => 'reverb', 
// ... 
'scaling' => [ 
'enabled' => true, 
'driver' => 'database', 
],
 ], 

Impact: for smaller applications and teams that want to keep infrastructure lean, this removes a Redis dependency that was previously required for scaling WebSocket connections across multiple servers. High-throughput applications with thousands of concurrent connections should still use Redis.
Monitoring and continuous optimization

15. Install Laravel Pulse for always-on monitoring

Pulse is Laravel's first-party, lightweight application monitoring dashboard, built to run continuously in production without the overhead of a full profiler.

composer require laravel/pulse 
php artisan vendor:publish --
provider="Laravel\Pulse\PulseServiceProvider" 
php artisan migrate 

Impact: gives you a live view of slow queries, slow endpoints, queue throughput, and exception rates without manually instrumenting your code, making it much easier to catch performance regressions before users report them.

16. Profile with Laravel Telescope or Blackfire in staging

Pulse is built for production monitoring at a glance. For deep, request-level profiling, use Telescope in staging or Blackfire for detailed call-graph analysis.

composer require laravel/telescope --dev 
php artisan telescope:install
php artisan migrate 

Impact: surfaces the exact queries, cache calls, and job dispatches behind any single request, which turns "this page feels slow" into a specific, fixable line of code instead of a guess.

How to use this checklist

Do not apply all sixteen items at once. Start by profiling with Telescope or Blackfire to find where time is actually being spent. Fix N+1 queries and missing indexes first since they typically offer the largest return for the least effort. Then move to caching and queues. Treat infrastructure changes like Octane and Redis as a second pass once the application-level issues are resolved, since they add operational complexity that is only worth taking on once the easier wins are in place.
Performance work is rarely a one-time project. The most reliable approach is to keep a monitoring tool like Pulse running permanently, so regressions are caught within days rather than discovered during a traffic spike.