9 Proven Techniques to Solve Laravel Production Performance Problems
This article outlines nine practical methods for diagnosing and resolving Laravel performance issues in production, covering request measurement, N+1 query elimination, query plan optimization, memory-efficient data processing, strategic caching, queue offloading, external API timeouts, framework bootstrapping optimizations, and log/payload reduction, with code examples and a step-by-step diagnostic checklist.
Laravel applications often run smoothly on a laptop but degrade in production after months of growth. The root cause is rarely the framework itself; instead, it stems from expanding database volumes, N+1 query patterns, oversized report queries, or third‑party APIs becoming critical‑path dependencies. Before reaching for Redis, Octane, or larger servers, the first step is to identify and remove unnecessary work — a change that often yields larger, safer gains than adding infrastructure.
1. Measure Slow Requests Before Changing Code
“Dashboard is slow” is not a diagnosis. Pick a single route, then record its response time, query count, cumulative SQL time, memory usage, and any external calls. Reproduce the issue with production‑like data. Laravel Telescope and Pulse help with controlled investigation; for requests spanning PHP, Redis, MySQL, queues, and external services, an APM trace is more useful. To catch exceptionally slow queries, listen for queries exceeding a threshold:
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,
]);
}
});Log only what matters — logging every query on a busy server creates noise, consumes disk I/O, and may leak sensitive values. Always compare the same request before and after each change; otherwise cache warm‑up or reduced database activity can make an ineffective fix appear successful.
2. Stop N+1 Queries Before They Multiply
N+1 queries are silent because the page still returns correct data. The innocent‑looking loop below runs one query for orders and then one customer query per order:
$orders = Order::latest()->take(100)->get();
foreach ($orders as $order) {
echo $order->customer->name;
}Switch to explicit eager loading with only the columns you need:
$orders = Order::query()
->select(['id', 'customer_id', 'total', 'created_at'])
->with('customer:id,name')
->latest()
->take(100)
->get();This typically reduces 101 queries to 2 and avoids fetching unused columns. Remember that Eloquent requires primary and foreign keys to match the models. For edge cases involving Blade, API resources, accessors, and regression tests, refer to the full Laravel N+1 detection and fixing guide; the official Eloquent relationships documentation is also a valuable reference for constrained and nested eager loading. To surface accidental lazy loading during development and CI, enable:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! app()->isProduction());
}Do not over‑eagerly load everything; pulling thousands of related models into memory can turn a query problem into a hydration problem.
3. Fix the Query Plan, Not Just the Eloquent Syntax
Once query count is under control, examine the slow SQL itself. Even a clean Eloquent chain can cause a full table scan. For a report that repeatedly filters paid orders by date:
Order::query()
->where('status', 'paid')
->whereBetween('created_at', [$from, $to])
->orderByDesc('created_at')
->get();A composite index matching the access pattern can help:
Schema::table('orders', function (Blueprint $table) {
$table->index(['status', 'created_at']);
});Run EXPLAIN with real values and check the chosen index, estimated rows, join order, and sort work. The MySQL query execution plan guide explains what the optimizer shows. Do not add indexes “just in case”; each unnecessary index consumes storage and adds overhead to inserts and updates.
4. Stop Loading Entire Tables into PHP Memory
Exports and maintenance commands often work in testing but fail later because get() loads the full 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);
}
}); paginate()suits regular screens, cursorPaginate() fits large ordered datasets, and lazyById() or chunkById() are for background jobs. Cursors use less memory but are not a silver bullet if every record requires a massive relationship graph.
5. Cache High‑Value Results with an Invalidation Plan
Caching helps when the underlying result is expensive and safe to reuse, but it does not turn a bad query into a good one.
$stats = Cache::remember(
"organization:{$organizationId}:dashboard-stats",
now()->addMinutes(10),
fn () => $this->buildDashboardStats($organizationId)
);Before deploying, answer three questions: what event invalidates the value? who is allowed to see it? what happens when multiple requests rebuild it simultaneously? Include tenant, permission, locale, and other relevant dimensions in the cache key. Invalidate or version the key when source data changes. For values that stampede on expiry under high traffic, use locks or a “stale‑while‑revalidate” strategy. Laravel’s cache documentation covers atomic locks and available stores. In multi‑server apps, use a shared store like Redis; local file caches give each server a different result.
6. Move Slow Work Out of the Web Request
Email sending, PDF generation, video processing, large imports, webhook exports, and search indexing belong in queues. The controller should validate, persist required state, dispatch the job, and return.
GenerateInvoicePdf::dispatch($invoice->id)->afterCommit(); afterCommit()prevents a fast worker from reading the record before the surrounding transaction commits. Queues move work; they do not remove it. Monitor queue depth, wait time, runtime, failed jobs, retries, and worker availability. A job that takes one second but waits 15 minutes due to exhausted queue capacity is still perceived as latency. When retry behavior misbehaves, inspect the original connection, queue name, worker configuration, timeout, backoff strategy, and retry_after values. The Laravel queue retry and failed job troubleshooting guide walks through these production cases without turning this article into a queue manual.
7. Put Boundaries Around External APIs
Even if your code is efficient, payment, CRM, logistics, or AI services can make a Laravel route slow. Set explicit connect and response timeouts:
$response = Http::connectTimeout(2)
->timeout(5)
->retry(2, 200)
->get($url);Retry only idempotent operations. For optional data, cache the last successful response or update it asynchronously. A controlled fallback is far better than letting an unhealthy vendor consume all PHP workers.
8. Apply Laravel and PHP Production Optimizations
Deployment must not force Laravel to rebuild framework metadata on real requests. Install production dependencies and cache the bootstrap files:
composer install --no-dev --prefer-dist --optimize-autoloader
php artisan optimizeThe optimize command caches config, events, routes, and views. Laravel’s official deployment optimization guide details each command and its environment caveats. Keep env() calls inside config files; once config is cached, direct env() calls elsewhere no longer load the .env file as expected. Production should also set:
APP_ENV=production
APP_DEBUG=falseEnable PHP OPcache and tune its size for the deployed codebase. OPcache reuses compiled bytecode, but a too‑small cache causes frequent churn and loses most of the benefit. Refer to the PHP OPcache configuration reference for settings. Finally, reload long‑running processes after deployment:
php artisan queue:restart
php artisan octane:reloadRun the Octane command only if the application uses Octane. Horizon‑managed workers should be terminated via Horizon so the process monitor restarts them with the new version.
9. Reduce Logging, Payload, and Serialization Waste
Debug logging inside loops can be extremely expensive. Keep logs structured, actionable, and rotated; avoid logging large request or response bodies unless an investigation demands it. Also inspect responses: API resources that attach unused attributes, serialize deep relationships, or return thousands of records waste database time, PHP memory, network bandwidth, and client rendering time all at once. Smaller responses are often the cheapest performance win across the entire request lifecycle.
Quick Production Diagnostic Order
When fixing a slow Laravel application without guesswork, follow this sequence:
Trace one slow route with real data.
Count queries and spot duplicate SQL.
Remove N+1 queries and unnecessary columns.
Run EXPLAIN on 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 for external calls.
Cache Laravel bootstrap and enable OPcache.
Repeat the measurement and compare.
Conclusion
The reliable way to solve Laravel production performance problems is to remove unnecessary work before adding capacity. Control query count, optimize database indexes, limit data entering PHP, and prevent slow dependencies from holding request workers indefinitely. Tackle one measured bottleneck at a time — this keeps changes reviewable, proves which optimizations actually help, and prevents hasty “performance fixes” from becoming permanent band‑aids.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
21CTO
21CTO (21CTO.com) offers developers community, training, and services, making it your go‑to learning and service platform.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
