A Laravel application that feels instant at 1,000 users can crawl at 100,000 — and the difference is rarely the framework. Laravel is fast out of the box; most performance problems come from how we query, cache, queue, and deploy. This is the field guide we use at Techglock when a Laravel app needs to scale: the specific levers, in the order we usually pull them, with the trade-offs that actually matter in production.

Whether you're on Laravel 11 or 12, the principles here hold. We'll move from the cheapest, highest-impact wins to the deeper architectural moves.

Start With Measurement, Not Guesswork

The first rule of optimization: don't. Not until you've measured. Reaching for a fix before you've profiled is how teams spend a week shaving 5ms off a page that's slow because of one N+1 query.

Our baseline toolkit:

  • Laravel Telescope in staging — see every query, job, and request with timings.
  • Laravel Pulse in production — a lightweight, always-on dashboard for slow queries, slow jobs, and high-usage endpoints.
  • Clockwork or the Debugbar locally — per-request query counts and durations.
  • An APM (New Relic, Datadog, or self-hosted) for real percentile latency — p95 and p99, not averages.
Optimize for p95, not the mean. A page that averages 200ms but spikes to 4s at p99 is a page your most engaged users hate. Averages hide the pain; percentiles reveal it.

Kill N+1 Queries First

The single most common Laravel performance bug is the N+1 query — loading a list of records, then firing one extra query per record for a relationship. It's invisible with 10 rows and catastrophic with 10,000.

The fix is eager loading:

// N+1 — one query for posts, then one per post for its author
$posts = Post::all();
foreach ($posts as $post) { echo $post->author->name; }

// Fixed — two queries total, regardless of row count
$posts = Post::with('author')->get();

Two habits prevent N+1 from ever reaching production:

  • Enable Model::preventLazyLoading() in your non-production environments — Laravel then throws an exception the moment a lazy load happens, so you catch it in development, not in an incident.
  • Reach for withCount(), withSum(), and loadMissing() instead of counting or summing relations in a loop.

Index the Database Like You Mean It

No amount of Eloquent cleverness beats a missing index. If a query filters, joins, or sorts on a column, that column usually needs an index. The symptoms of a missing index are unmistakable: a query that's fast on your dev machine and slow on production, where the table is 500× bigger.

Practical rules:

  • Index every foreign key and every column in a frequent WHERE, ORDER BY, or JOIN.
  • Use composite indexes for multi-column filters, in the order the query uses them (leftmost-prefix rule).
  • Run EXPLAIN on your slowest queries — if you see "Using filesort" or a full table scan on a big table, you've found your bottleneck.
  • Select only the columns you need. select('id', 'name') beats hydrating 40-column models you'll never fully use.

Cache the Expensive, Invalidate the Precise

Caching is where Laravel apps get their biggest speed jumps — and their nastiest bugs. The goal is to cache expensive, rarely-changing work and invalidate it surgically.

Application cache

Use a real driver — Redis — for cache, sessions, and queues in production. The default database driver is fine for local work but becomes a bottleneck under load. Wrap expensive reads in Cache::remember() and invalidate on write, keyed tightly so one update doesn't blow away unrelated data.

Config, route, and view caching

This is free performance you should never skip in production:

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

These collapse dozens of file reads and reflection calls into a single cached artifact per request. Combined with OPcache enabled at the PHP level, they're often the biggest TTFB win available — with zero application code changes.

Move Slow Work Off the Request

Anything a user doesn't need to wait for should not run inside the request. Sending email, generating PDFs, calling third-party APIs, processing uploads, warming caches — push it all to a queue.

// Blocks the response until the email is sent
Mail::to($user)->send(new WelcomeEmail);

// Returns instantly; the worker sends it
Mail::to($user)->queue(new WelcomeEmail);

Run Laravel Horizon on top of Redis queues for visibility, auto-balancing, and metrics. Size your worker pools to the work, and monitor queue wait time as a first-class metric — a growing backlog is an early warning of trouble.

Serve Static and Cacheable Responses at the Edge

The fastest request is the one your PHP never handles. Put a CDN in front of assets, and for truly public, anonymous pages, consider full-page caching (LiteSpeed Cache, Varnish, or a Cloudflare rule) so repeat hits never reach the origin. The catch: any page with per-user state or CSRF forms can't be shared-cached, and cached hits won't run your analytics — so cache deliberately, page by page.

A Sensible Optimization Order

StepTypical effortTypical payoff
Fix N+1 queriesLowVery high
Add missing indexesLowVery high
config/route/view cache + OPcacheLowHigh (TTFB)
Redis for cache/session/queueLow–MediumHigh
Queue slow work + HorizonMediumHigh (perceived speed)
Application-level cachingMediumHigh
Edge / full-page cachingMedium–HighVery high (public pages)

Deploy Without Downtime

Performance isn't only about speed — it's also about not dropping requests during releases. Use atomic, zero-downtime deploys (Envoyer, Deployer, or a CI pipeline that symlinks a fresh release directory) so a deploy is an instant pointer switch, not a window of 500s. Always run the cache-warming artisan commands as the final deploy step.

Frequently Asked Questions

What is the single most impactful Laravel performance fix?

Eliminating N+1 queries, closely followed by adding missing database indexes. Together they resolve the majority of "my Laravel app got slow as it grew" problems, and both are low-effort. Enable preventLazyLoading() in development so N+1s can never reach production unnoticed.

Do I need Redis, or is the database cache enough?

For local development the database driver is fine. In production, use Redis for cache, sessions, and queues — it's dramatically faster under concurrency and prevents your cache/session load from competing with real application queries on the same database.

Will config:cache and route:cache break anything?

No, as long as you don't call env() outside config files (once config is cached, env() returns null elsewhere). Read config values via config(), run the cache commands on every deploy, and you get a meaningful TTFB improvement for free.

How do I know if caching is helping or hurting?

Measure p95/p99 latency before and after, and watch your cache hit ratio. If hit ratio is low or you're seeing stale data bugs, your cache keys or invalidation are too broad. Cache expensive, slow-changing work; invalidate precisely on write.

The Bottom Line

Laravel scales well when you respect the fundamentals: measure first, kill N+1s, index properly, cache the expensive work, queue the slow work, and deploy atomically. None of it requires exotic infrastructure — most of the biggest wins are a day's work and ship with the framework.

Need a Laravel app profiled and hardened for scale? Talk to the Techglock engineering team — we do performance audits, queue architecture, and zero-downtime deployment setups for production Laravel systems.