Laravel Performance Optimization In 2026: How To Turn A Slow Application Into A Fast One

Somewhere in your Laravel application there is a page that takes six seconds to load. Nobody wants to admit it. The dashboard “works.” The API “responds.” But every user feels the drag, and every sprint adds another feature on top of a slow foundation. Performance debt is the quiet kind of debt. It never throws an exception. It just slowly costs you customers, cloud budget, and engineering morale.

The good news is that Laravel performance work is far more predictable than most teams assume. In our experience, roughly 80% of the slowness in a typical mid-size Laravel application traces back to a short list of well-known problems: unindexed queries, N+1 leaks, missing caches, background work running on the request thread, and a server that was never tuned for the load it now carries. None of that requires a rewrite. It requires discipline.

This guide is that discipline, written out end to end. It is the same process we follow when we inherit a Laravel codebase that grew faster than the engineering practices around it. You will find concrete numbers, real tooling, and a sequenced plan you can start this week.

Why Laravel Performance Problems Are Almost Never The Framework’s Fault

The first reaction of many teams is to blame the framework. “Laravel is slow.” It is not. Out of the box, a clean Laravel 11 application can serve thousands of requests per second on modest hardware. The framework is not the bottleneck. Your query patterns are.

Laravel makes hard things easy, and that is precisely where trouble hides. Eloquent turns a complex join into one readable line. Collections turn a loop into a chain. Jobs turn one line into background work. Every one of those conveniences has a cost, and the cost only shows up under real traffic.

Where the time actually goes

Before touching any code, understand that a Laravel request is a pipeline. Time is consumed in layers, and the layers are rarely balanced. In a typical slow application we profile, the breakdown looks something like this:

Layer Typical healthy share What we usually see when things are slow
PHP bootstrap and middleware 5 to 10% 15 to 30% when config caching is off
Database queries 20 to 35% 55 to 75% with N+1 and missing indexes
Cache and Redis calls 5 to 10% Near zero, because caching was never added
View rendering and serialization 10 to 20% 20 to 30% with unoptimized Blade or heavy JSON casts
External HTTP calls 5 to 15% Synchronously blocking the whole request

Read that table one more time. In almost every slow Laravel app, the database is the story. That is why we always start there, and it is why “we need more servers” is usually the wrong first answer.

Measure Before You Optimize

Optimizing without measurement is guessing, and guessing wastes weeks. The teams that fix performance quickly all share one habit: they instrument first, then change exactly one thing, then re-measure.

The four numbers that matter

  • p50 and p95 latency per endpoint. Averages lie. The median tells you what most users feel, and the 95th percentile tells you who is about to complain.
  • Queries per request. This single number catches more problems than anything else. A healthy endpoint runs under 10 queries. A dashboard running 400 queries is a dashboard that was never reviewed.
  • Time to first byte from the database. If a query takes 900 milliseconds, no amount of PHP tuning will save you.
  • Queue depth and job duration. Background work that piles up is a symptom you can see hours before users notice the outage.

The tooling stack

You do not need anything exotic. Laravel Debugbar and Telescope give you instant visibility into queries, cache hits, and slow jobs during development. For production, reach for XHProf, Blackfire, or an OpenTelemetry exporter feeding into Grafana. Laravel Pulse, which stabilizes in 2026, gives you a first-party dashboard for slow queries and slow outgoing requests.

One rule: never leave a heavy profiler running in production. Measure in staging with realistic data, or use sampling in production. A profiler that doubles your latency is its own incident waiting to happen.

Kill N+1 Queries First

If you fix nothing else, fix this. The N+1 problem is the most expensive habit in Laravel, and it is almost invisible in a development environment with a dozen rows in each table.

How it happens

Consider a simple invoice list. You load 50 invoices, then inside a Blade loop you call $invoice->customer->name. Because the relationship was not eager loaded, Laravel fires one query for the customers. Fifty invoices, one query each, plus the original. That is 51 queries for a page that should need two.

On a laptop with local MySQL, those 51 queries finish in 40 milliseconds and nobody notices. On a network round trip of 2 milliseconds, with a real dataset of 2,000 invoices, the same pattern produces 2,001 queries and a page that takes four seconds. The code never changed. Only the data did.

The fix

Eager load relationships with with(), and keep an eye on nested relationships.

  • Replace lazy relationship access inside loops with a single with([‘customer’, ‘items.product’]) call.
  • Enable strict mode in local development with Model::preventLazyLoading() so a lazy load throws instead of hiding.
  • Add a query-count assertion in your test suite for your most important endpoints. Fail the build when an endpoint exceeds its budget.

In one in-house audit, adding eager loading to a reporting screen cut its query count from 1,240 to 9 and its response time from 5.8 seconds to 340 milliseconds. That is not a rewrite. That is one afternoon.

The Database Layer: Indexes, Query Shape, And Chunking

Even with eager loading, a database can be misconfigured. This layer is where careful engineering pays the highest dividend, because the same query can differ by two orders of magnitude depending on how it is written.

Indexes that match your filters

An index on a single column helps only when that column leads your filter. If your application constantly queries orders by status and created_at, a composite index on both columns can turn a full table scan into an index range scan. Watch for these traps:

  • Functions wrapped around an indexed column, such as WHERE DATE(created_at) = …, which disable the index entirely. Rewrite as a range comparison.
  • Leading wildcards in LIKE ‘%term’, which no index can serve. Use a full-text index or a dedicated search service.
  • Low-cardinality boolean columns indexed alone, which the optimizer often ignores.
  • Missing foreign key indexes after a schema evolution. Laravel migrations do not always add them when you expect.

Stop selecting everything

SELECT * feels harmless. It forces the database to read and ship columns you never use, and it breaks covering indexes. Ask for the columns you need. On wide tables with JSON columns, the difference can be substantial.

Chunk large datasets

Loading a million rows into memory is a guaranteed way to hit PHP’s memory limit. Laravel gives you two clean tools:

  1. chunk() when you mutate records and can tolerate a stable order, processing a few hundred at a time.
  2. lazy() when you only read, which keeps one model in memory at a time via a generator.

For exports and reports, push the work into a queued job so a web request is never blocked by a scan over a large table.

A Caching Strategy That Actually Works

Caching is the highest-leverage change most Laravel applications can make, and also the most dangerous if done carelessly. The goal is not “cache everything.” The goal is to cache the expensive, slow-changing reads and leave the rest alone.

Cache in layers

  • Compiled caches. Run php artisan config:cache, route:cache, view:cache, and event:cache in production. Skipping these can add 30 to 60 milliseconds to every request before your code even runs.
  • OPcache. Enable it with a sensible memory ceiling. This alone often halves PHP execution time compared to interpreting every file.
  • Application cache. Use Redis rather than the file driver in production. Reach for Cache::remember() around expensive query results and API responses.
  • HTTP cache and CDN. Static assets should never hit PHP. Set long cache lifetimes with content hashes and let a CDN do the rest.

Invalidation and cache tags

Stale data is worse than slow data. Use cache tags on Redis so you can flush a related group in one call when something changes. Give every entry a TTL. And never cache responses that depend on the authenticated user without including the user identifier in the key, or you will ship one user’s data to another.

A well-instrumented Laravel app should show a cache hit rate above 85% for its hot read paths. If Redis is sitting idle while your database sweats, you have left easy wins on the table.

Move Slow Work Into Queues

Anything that touches an external service, generates a file, sends an email, or processes a large batch does not belong on the request thread. The user is waiting. Free them.

What belongs in a queue

  • Outbound emails, SMS, and push notifications.
  • Image resizing, PDF generation, and report exports.
  • Third-party API calls, especially ones with unpredictable latency.
  • Webhook processing and any fan-out to other systems.

Run queues properly

Use Redis as the queue backend and Laravel Horizon to supervise workers. Horizon gives you throughput, queue depth, and failed-job visibility in one place. Size your workers to the workload: I/O-bound jobs can run many per CPU core, while CPU-heavy jobs should be limited to avoid starving the web tier. Add a retry_after that exceeds your longest job, or you will process the same job twice and wonder why emails arrive in duplicate.

One caution from the field: a queue is not a garbage can. If jobs pile up faster than they drain, you have not solved a performance problem, you have scheduled a bigger one for later. Watch the depth, not just the throughput.

Laravel Octane: The Persistent Runtime Question

Laravel Octane keeps the application booted between requests using Swoole or FrankenPHP. Instead of rebuilding the framework container on every hit, it reuses a warm process. The gains are real: teams commonly report two to four times more throughput on the same hardware for well-behaved applications.

But Octane changes the rules, and it is not free.

  • State leaks between requests. Anything you assign to a static property or a singleton survives into the next request. Audit your service container for mutable state.
  • Code changes require a worker reload. Your deploy pipeline must gracefully restart Octane workers, not just deploy files.
  • Not all packages are Octane-safe. Test your dependency list before committing, and pin versions you have verified.

Our rule of thumb: adopt Octane when you are CPU-bound on framework bootstrap and your application is disciplined about state. If your database is the bottleneck, Octane will only let you hit the same wall faster.

Frontend And Asset Delivery

Backend latency is half the user’s experience. The other half is everything the browser does. A page that responds in 200 milliseconds but takes three seconds to render still feels slow.

  • Ship already-built assets. Do not compile Vite assets on the production server during a request.
  • Split bundles and lazy-load routes the user has not visited yet.
  • Serve images in modern formats and correct dimensions. A 4 MB hero image costs more than any database index will save you.
  • Set Cache-Control aggressively on hashed assets and let a CDN absorb the traffic.

Infrastructure And Server Tuning

Eventually you must tune the machine. This is the least glamorous work and the most measurable, because the numbers are concrete.

PHP-FPM sizing

The pm.max_children setting decides how many requests run concurrently. Set it too low and requests queue; set it too high and the machine swaps. A practical starting point is total available memory divided by the average worker size, then adjust from real measurements. Pair it with pm.max_requests to recycle workers and cap memory growth from small leaks.

Database and Redis

  • Give MySQL an InnoDB buffer pool sized to hold your working set in memory. Nothing beats a hot buffer pool.
  • Turn on the slow query log with a low threshold and review it weekly. It is the cheapest performance monitoring you will ever run.
  • Keep Redis on the same private network, never across the public internet, and monitor its memory ceiling.

If the workload is steady and predictable, a well-tuned single box often beats a premature cluster. Scale horizontally when you have measured a genuine ceiling, not before.

A Real Optimization Case Study

Numbers make the argument better than adjectives, so here is a recent engagement reduced to its essentials. A B2B ordering platform on Laravel was carrying about 1.2 million orders and serving roughly 400 requests per second at peak. The product team wanted to add three new features but could not, because the server was permanently pinned at 95% CPU.

We profiled for two days and found the usual suspects: an order history endpoint running 1,800 queries per request, no OPcache, uncached configuration, and PDF invoices generated synchronously on the request thread.

Change Effort Measured result
Eager load and restructure the order history query 1 day 1,800 queries to 14; p95 dropped from 4.9s to 410ms
Enable OPcache and compiled caches 2 hours Throughput up 38%; CPU down 22 points at peak
Move invoice generation to a queued job half a day Removed a 2.3s synchronous spike from the checkout path
Add Redis caching to the catalog and pricing reads 2 days Database load down 46%; p50 down 180ms

Total elapsed time: under one week. No framework migration, no rewrite, no new cluster. Peak CPU settled near 40%, and the team shipped the three delayed features the following sprint. This is what disciplined Laravel performance work looks like. It is boring, incremental, and enormously effective.

A Practical 30-Day Optimization Roadmap

If you want to start today and stay honest about progress, work in this order. Measure before and after every step. This is the same sequence professional Laravel teams run, and the one behind the production products you will find at pagii.co.

  1. Days 1 to 3. Instrument the application. Add slow query logging, enable Pulse or Telescope in staging, and record your baseline p50, p95, and query counts for the ten most important endpoints.
  2. Days 4 to 8. Fix N+1 queries and add eager loading where the numbers point.
  3. Days 9 to 14. Add missing indexes, remove SELECT * from hot paths, and refactor any query that wraps a column in a function.
  4. Days 15 to 18. Enable OPcache and compiled caches in production. Confirm the deploy pipeline rebuilds them correctly.
  5. Days 19 to 24. Introduce Redis caching on the slow, stable read paths, with tags and TTLs.
  6. Days 25 to 28. Move blocking work into queues and confirm Horizon shows a healthy drain rate.
  7. Days 29 to 30. Re-measure, document the before and after, and set query-count budgets as tests so the gains do not quietly erode.

Fix It Yourself, Or Bring In Help

Performance work is a skill, and it is a skill that improves with repetition. If your team ships Laravel every day, they can learn it. The roadmap above is not secret knowledge. But there is a real threshold where an experienced outside team pays for itself, because they have already made the mistakes you are about to make.

Consider outside help when the application is too slow to be improved safely, when the codebase has no tests, or when the business cannot afford a four-week learning curve. A specialist team can profile, prioritize, and hand back a documented plan with measured results. That is the kind of work we do at pagii.co, and it is the same discipline behind the products we run ourselves.

Practical products built for heavy, real-world traffic are a good proof of process. We operate transactional systems that handle everything from ordering to HR to digital stamps, and those workloads do not tolerate sluggish code. If a team can keep those products responsive under load, they can almost certainly do the same for yours. In either case, whether you fix it in-house or bring in a partner, the sequence is the same: measure, fix the database, cache what is slow and stable, queue the rest, then tune the machine.

Frequently Asked Questions

How much can Laravel performance really improve without a rewrite?

Far more than most teams expect. In the majority of Laravel applications we audit, careful query optimization, caching, and server tuning deliver a 3x to 8x improvement in p95 latency with zero framework migration. Rewrites are almost never the first answer. They are what teams reach for when they have not yet measured the real problem.

Is Laravel slower than Node.js or Go for APIs?

A raw Go service will outrun a PHP-FPM application on CPU-bound work, and a well-written Node.js service is competitive for I/O-heavy endpoints. But Laravel’s architecture is rarely the limiting factor in a business application. The database and your query patterns dominate the numbers. Choose Go or Node when raw throughput is genuinely the product constraint; otherwise optimize the framework you already run, because rewriting carries its own enormous cost.

Should I enable Laravel Octane right away?

Only after your application is clean. Octane amplifies a well-behaved application and punishes a messy one, because in-memory state leaks between requests. Fix your N+1 queries and caching first. Then adopt Octane once you are confident your code holds no mutable global state and your deploy pipeline restarts workers cleanly.

How many queries per request is acceptable?

For most pages, aim under 10. A simple detail page should use 2 to 4. Complex dashboards can justify 20 to 30 with caching layered on top. If an endpoint crosses 100 queries, treat it as a bug and fix it before adding features. Set this as a test budget so regressions fail the build.

What is the fastest single win for a slow Laravel app?

Enable OPcache and run the compiled config, route, and view caches in production, then fix N+1 queries on your busiest endpoints. Those two steps typically account for the largest, easiest gains and cost almost nothing in engineering time. They are the first things we check in every audit.

How do I keep performance from degrading again?

Make it visible and make it automatic. Add query-count budgets to your test suite, review the slow query log weekly, and set an alert on p95 latency. Performance that is not measured will quietly decay with every new feature. The teams that stay fast are the ones that treat latency as a product requirement, not an afterthought.

Conclusion

Laravel performance is not a mystery, and it is not the framework’s fault. It is a sequence of known problems with known fixes, and the only real variable is whether your team measures before it optimizes. The slow page in your application was almost certainly written by capable people under deadline pressure. Nobody set out to build something slow.

That is why the fix is methodical rather than heroic. Measure the endpoints that matter. Fix the N+1 queries that multiply your database calls. Add the indexes that make your filters fast. Cache the reads that are expensive and stable. Move the blocking work into queues. Tune the server only after the code is clean, and consider Octane only after the application respects its own memory. Do that in order, and the gains compound faster than any feature you could ship.

The teams that win on performance are not the ones with the cleverest tricks. They are the ones that refuse to guess. They open a profiler, read the numbers, change one thing, and prove it helped. Start this week with your three slowest endpoints, and you will understand exactly why the framework was never the problem. The foundation is solid. All that is missing is the discipline to build on it properly.

Leave a Reply