Laravel performance optimization has a reputation for being about exotic tools, but in practice most slow Laravel applications are slow for boring reasons: N+1 queries, missing indexes, work done on every request that could be cached, and production servers running with development settings. This post walks through the optimizations in the order you should actually apply them β€” measured impact first, shiny infrastructure last.

Step zero: measure before you touch anything

Optimizing without measurement is how teams spend a sprint shaving milliseconds off a page nobody visits. Install Laravel Debugbar or Telescope locally to see query counts and timings per request, and put Laravel Pulse on production β€” it shows slow requests, slow queries, slow jobs, and cache hit rates on a single dashboard with negligible overhead. Once you can see where time goes, the fixes below stop being guesses.

Step zero: measure before you touch anything β€” Laravel Performance Optimization: Caching, Octane, and Query Tuning
Step zero: measure before you touch anything

Fix the database first

In a typical Laravel request, the database is 80 percent of the wall time, so it is where optimization starts.

Kill N+1 queries

The classic mistake is looping over models and touching a relationship inside the loop, turning one query into hundreds. Eager load instead:

// 1 + N queries
$posts = Post::latest()->take(20)->get();
foreach ($posts as $post) {
    echo $post->author->name;
}

// 2 queries
$posts = Post::with('author')->latest()->take(20)->get();

Make the mistake impossible to miss by enabling strict mode in non-production environments inside AppServiceProvider::boot(): Model::shouldBeStrict(! app()->isProduction());. Lazy loading then throws an exception in development and tests instead of silently degrading.

Select less, index more

Fetch only the columns you render (->select('id', 'title', 'created_at')), use withCount('comments') instead of loading whole relationships just to count them, and check any query in a hot path with ->explain(). A missing index on a foreign key or a frequently filtered column is routinely a 100x improvement β€” no PHP-level optimization comes close. For large exports or backfills, stream with lazyById() or process in chunkById() batches rather than calling ->get() on a million rows.

Cache expensive work, not everything

Laravel\'s cache API makes the right pattern trivial:

$stats = Cache::remember('dashboard:stats', now()->addMinutes(10), function () {
    return [
        'revenue' => Order::whereMonth('created_at', now()->month)->sum('total_cents'),
        'signups' => User::whereDate('created_at', today())->count(),
    ];
});

Good caching candidates share three traits: expensive to compute, read far more often than they change, and tolerant of slight staleness. Dashboard aggregates, navigation menus built from the database, settings tables, and third-party API responses all qualify. A few practical notes:

  • Use Redis in production. The file driver serializes access and the database driver adds load to the thing you are protecting. Redis also unlocks atomic locks and tagged caches.
  • Prevent cache stampedes on very hot keys with Cache::flexible(), which serves slightly stale data while one process refreshes in the background, or with Cache::lock() around the recompute.
  • Invalidate on write, expire as a backstop. Forget keys in the model\'s updated event or observer, and keep a TTL anyway so bugs heal themselves.
  • Sessions and queues belong in Redis too β€” file sessions block concurrent requests from the same user, which shows up as mysterious slowness on dashboards that fire parallel AJAX calls.

The production checklist: free speed you must opt into

These are the settings that separate a tuned production box from a default one, and every deploy script should run them:

The production checklist: free speed you must opt into β€” Laravel Performance Optimization: Caching, Octane, and Query Tuning
The production checklist: free speed you must opt into
php artisan config:cache   # collapses all config files into one cached array
php artisan route:cache    # compiles route registration into a single file
php artisan view:cache     # precompiles Blade templates
php artisan event:cache    # caches event-listener discovery
composer install --no-dev --optimize-autoloader

Or simply php artisan optimize, which runs the cache steps together on modern Laravel. Config caching alone eliminates dozens of file reads and every env() call per request β€” which is also why env() must never appear outside the config directory: once the config is cached, it returns null. Pair this with OPcache properly configured in production (validate timestamps off, revalidate on deploy) so PHP stops re-parsing files entirely. These steps typically cut bootstrap time in half before you touch any application code.

Move work off the request path

Anything the user does not need to wait for belongs on a queue: emails, PDF generation, image processing, webhook fan-out, search indexing. A checkout that sends a confirmation email inline is donating 300 to 800 milliseconds of SMTP latency to every customer. Dispatch a job, return the response, and let a worker handle it. Combined with HTTP-level tricks β€” pagination instead of unbounded lists, defer() for after-response work, and full-page or fragment caching for anonymous traffic β€” the request path should only contain work whose result the user is actually looking at.

Octane: when and why

Laravel Octane keeps the framework booted in memory using Swoole, FrankenPHP, or RoadRunner, serving requests from long-lived workers instead of bootstrapping on every hit. The gain is real: the ~20 to 50 milliseconds a tuned app spends on bootstrap drops to near zero, and throughput per server rises accordingly. But be honest about the math β€” if your requests spend 400 milliseconds in unoptimized queries, Octane makes them 380 milliseconds. It amplifies an already-fast application; it does not rescue a slow one.

Octane: when and why β€” Laravel Performance Optimization: Caching, Octane, and Query Tuning
Octane: when and why

Octane also changes the programming model. Workers persist between requests, so state you stash in static properties or singletons leaks from one request to the next. Audit for memoized user data, register singletons carefully, and rely on Octane\'s per-request container flushing. The payoff, once clean, extends beyond speed: concurrent task execution with Octane::concurrently(), a shared in-memory cache, and dramatically better tail latency under load. Adopt it when bootstrap time is actually your bottleneck β€” high-traffic APIs, latency-sensitive endpoints β€” and only after the database and cache layers are in order.

A sensible order of operations

  1. Install Pulse or Telescope and find the slowest 10 endpoints.
  2. Eliminate N+1s and add missing indexes on those endpoints.
  3. Run the production optimization commands in your deploy script and verify OPcache settings.
  4. Move Redis into place for cache, sessions, and queues; cache the expensive computations you measured.
  5. Queue everything the user does not wait for.
  6. Reach for Octane, HTTP caching, or read replicas once the fundamentals are exhausted.

Performance work in Laravel rewards discipline more than cleverness. Measure, fix the database, cache deliberately, ship the production checklist β€” and by the time you genuinely need Octane, you will have an application worth accelerating.

Related Service

πŸ’» Web Development

Custom websites and web applications built with PHP, Laravel, WordPress, and React β€” fast, secure, scalable, and tailored to your business goals.

Explore Web Development →
Share this article
X Facebook LinkedIn