9 Practical Ways to Fix Laravel Performance Issues in Production
Learn how to fix Laravel performance issues in production using practical solutions for slow queries, N+1 problems, caching, queues, APIs, and deployment.
Join the DZone community and get the full member experience.
Join For FreeA Laravel application can feel instant on a developer laptop and painfully slow after a few months in production. The framework is rarely the real problem. More often, the database has grown, a relationship is being loaded inside a loop, a report pulls far more rows than anyone expected, or a third-party API has quietly become part of the request's critical path.
If you are trying to learn how to fix Laravel performance issues in production, resist the urge to begin with Redis, Octane, or a larger server. First, find the work the request shouldn't be doing. Removing wasted work usually produces a bigger, safer improvement than adding infrastructure around it.
Here is the production-focused sequence I would use.
1. Measure the Slow Request Before Changing Code
“The dashboard is slow” is not a diagnosis. Pick one route, record its response time, database query count, cumulative SQL time, memory usage, and any external calls. Then reproduce it with production-like data.
Laravel Telescope and Pulse can help during controlled investigation. An APM trace is even more useful when the request crosses PHP, Redis, MySQL, queues, and third-party services. For a narrow database check, listen for unusually slow queries:
use Illuminate\Database\Events\QueryExecuted;
use Illuminate\Support\Facades\DB;
DB::listen(function (QueryExecuted $query) {
if ($query->time >= 200) {
logger()->warning('Slow database query', [
'sql' => $query->toRawSql(),
'time_ms' => $query->time,
]);
}
});
Keep this targeted. Logging every query on a busy server creates noise, consumes disk I/O, and may record values that should not live in application logs.
One more detail matters: compare the same request before and after each change. Otherwise, a warm cache or quieter database can make an ineffective fix look successful.
2. Stop N+1 Queries Before They Multiply
N+1 queries are easy to miss because the page still returns the correct data. This innocent-looking code can execute one order query followed by one customer query for every order:
$orders = Order::latest()->take(100)->get();
foreach ($orders as $order) { echo $order->customer->name; }
Load the relationship deliberately:
$orders = Order::query()
->select(['id', 'customer_id', 'total', 'created_at'])
->with('customer:id,name')
->latest()
->take(100)
->get();
That usually turns 101 queries into two. It also avoids retrieving columns the response never uses. Remember to keep the primary and foreign keys Eloquent needs to match the models.
For the edge cases involving Blade, API resources, accessors, withCount(), and regression tests, use this complete guide to detecting and fixing Laravel N+1 problems. Laravel's official Eloquent relationship documentation is a useful reference for constrained and nested eager loading.
During development and CI, make accidental lazy loading visible:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}
Do not respond by eager-loading everything. Pulling thousands of related models into memory can replace a query problem with a hydration problem.
3. Fix the Query Plan, Not Just the Eloquent Syntax
Once the query count is under control, inspect the slow SQL itself. A clean Eloquent chain can still force the database to scan a large table.
Suppose a production report repeatedly filters paid orders by date:
Order::query()
->where('status', 'paid')
->whereBetween('created_at', [$from, $to])
->orderByDesc('created_at')
->get();
A composite index may fit that access pattern:
Schema::table('orders', function (Blueprint $table) {
$table->index(['status', 'created_at']);
});
“May” is important. Run EXPLAIN with realistic values and inspect the chosen index, estimated rows, join order, and sorting work. The MySQL guide to query execution plans explains what the optimizer is showing you.
Do not collect indexes as insurance. Every unnecessary index consumes storage and adds work to inserts and updates.
4. Stop Loading Entire Tables Into PHP Memory
Exports and maintenance commands often work during testing and fail later because get() loads the complete result set.
Process large tables in stable batches:
Order::query()
->where('status', 'pending')
->chunkById(500, function ($orders) {
foreach ($orders as $order) {
ProcessPendingOrder::dispatch($order->id);
}
});
Use paginate() for normal screens, cursorPaginate() for large ordered feeds, and lazyById() or chunkById() for background processing. A cursor is memory-friendly, but it is not a magic solution when every record needs a large relationship graph.
5. Cache Expensive Results With an Invalidation Plan
Caching helps when the underlying result is expensive and safe to reuse. It does not make a bad query good.
$stats = Cache::remember(
"organization:{$organizationId}:dashboard-stats",
now()->addMinutes(10),
fn () => $this->buildDashboardStats($organizationId)
);
Before shipping this, answer three questions: What event makes the value stale? Who is allowed to see it? What happens when many requests rebuild it at once?
Include tenant, permission, locale, and other relevant dimensions in the cache key. Forget or version the key when its source data changes. For a value that expires under heavy traffic, use a lock or stale-while-revalidate strategy to prevent a cache stampede. Laravel's cache documentation covers atomic locks and the available stores.
On a multi-server application, use a shared store such as Redis. A local file cache gives each server a different answer.
6. Move Slow Work Out of the Web Request
Email delivery, PDF generation, video processing, large imports, webhook fan-out, and search indexing normally belong in queues. The controller should validate the request, commit the required state, dispatch the work, and return.
GenerateInvoicePdf::dispatch($invoice->id)->afterCommit();
afterCommit() prevents a fast worker from reading records before the surrounding transaction is committed.
Queues do not remove work; they move it. Monitor queue depth, wait time, run time, failed jobs, retries, and worker availability. A one-second job is still late when it waits 15 minutes behind an under-provisioned queue.
When retries behave strangely, check the original connection, queue name, worker configuration, timeout, backoff, and retry_after values. This Laravel queue retry and failed-job troubleshooting guide walks through those production cases without turning this performance article into a queue manual.
7. Put Boundaries Around External APIs
A payment, CRM, shipping, or AI service can make your Laravel route slow even when your own code is efficient. Set explicit connection and response timeouts:
$response = Http::connectTimeout(2)
->timeout(5)
->retry(2, 200)
->get($url);
Retry only operations that are safe to repeat. For optional information, cache the last successful value or update it asynchronously. A controlled fallback is better than letting one unhealthy vendor consume every PHP worker.
8. Apply Laravel and PHP Production Optimizations
A deployment should not leave Laravel rebuilding framework metadata on live requests. Install production dependencies and cache the application bootstrap files:
composer install --no-dev --prefer-dist --optimize-autoloader
php artisan optimize
The optimize command caches configuration, events, routes, and views. Laravel documents the individual commands and environment caveats in its official deployment optimization guide.
Keep env() calls inside configuration files. Once configuration is cached, the .env file is not loaded in the normal way for direct calls elsewhere in the application.
Production should also have:
APP_ENV=production
APP_DEBUG=false
Enable PHP OPcache and size it for the deployed codebase. OPcache reuses compiled PHP bytecode, but an undersized cache can churn and lose much of the benefit. The relevant settings are listed in the PHP OPcache configuration reference.
Finally, reload long-running processes after deployment:
php artisan queue:restart
php artisan octane:reload
Run the Octane command only when the application uses Octane. Horizon-managed workers should be terminated through Horizon so the process monitor starts them with the new release.
9. Reduce Logging, Payload, and Serialization Waste
Debug logging inside a loop can become surprisingly expensive. Keep logs structured and actionable, rotate them, and avoid recording large request or response bodies unless an investigation genuinely requires it.
Look at the response too. API resources that append unused attributes, serialize deep relationships, or return thousands of records waste database time, PHP memory, network bandwidth, and client rendering time in one move.
Smaller responses are often the cheapest performance win in the entire request.
A Fast Production Diagnosis Order
When you need to fix a slow Laravel application without making random changes, use this order:
- Trace one slow route with realistic data.
- Count queries and find repeated SQL shapes.
- Remove N+1 queries and unnecessary columns.
- Run
EXPLAINon the remaining slow statements. - Paginate, stream, or chunk oversized results.
- Cache only stable, expensive values.
- Queue work the user does not need immediately.
- Add timeouts and fallbacks to external calls.
- Cache Laravel bootstrap files and enable OPcache.
- Repeat the same measurement and compare the result.
Conclusion
The reliable way to fix Laravel performance issues in production is to remove unnecessary work before adding more capacity. Keep query counts bounded, make the database's job index-friendly, control how much data enters PHP, and stop slow dependencies from occupying request workers indefinitely.
Fix one measured bottleneck at a time. That keeps the change reviewable, proves which optimization worked, and prevents a rushed “performance pass” from becoming a collection of permanent workarounds.
Published at DZone with permission of Ankit Khoiwal. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments