Why Your Laravel App Slows Down After 10,000 Users (And How to Fix It)

TECHNICAL GUIDE ✦ LARAVEL ✦ PERFORMANCE FOR DEVELOPERS AND CTOs 12 min read · 2026

Most Laravel apps perform well with a few hundred users and start struggling somewhere between 5,000 and 15,000 active users. The causes are almost always the same five problems — and all of them are fixable without rewriting your application.

5 Root causes behind most slowdowns
N+1 The most common performance killer
~90% Cases fixed without rewriting
23 yrs PHP and Laravel experience at Infomaze
— Why This Happens at This Scale

Laravel out of the box is not configured for production load.

A fresh Laravel installation is configured for developer convenience, not production performance. Configuration caching is off. Query logging is on. Eager loading isn't enforced. Every request boots the full framework from scratch. For a development environment or an early-stage app with a few hundred users, none of this matters.

At 10,000 active users, the patterns that were invisible become visible. A query that runs 47 times per page request because of an N+1 problem was running 47 times at 100 users too — you just didn't notice it at that scale. At 10,000 concurrent users, each running 47 queries, you're looking at nearly half a million database queries per second at moderate load. Your database slows down. Response times go up. Users leave.

The good news: most Laravel performance problems at this scale don't require architectural changes. They require fixing the same five problems that appear in almost every application that hasn't been specifically performance-tuned.

— Problem 1 — The N+1 Query Problem

The most common cause of Laravel slow pages. Almost always fixable with one line.

The N+1 query problem: you load a list of N records, then for each record you run an additional query to load a related record. Result: 1 query to get the list, N queries to get the related data. With 100 orders on a page, that's 101 queries. With 1,000, it's 1,001.

N+1 problem — the pattern that kills performance
// THIS IS THE PROBLEM — runs 1 + N queries
$orders = Order::all();

foreach ($orders as $order) {
    echo $order->customer->name;  // Query runs for EVERY order
}

// THIS IS THE FIX — runs 2 queries total (eager loading)
$orders = Order::with('customer')->get();

foreach ($orders as $order) {
    echo $order->customer->name;  // No additional query
}

// FOR DEEPLY NESTED RELATIONSHIPS
$orders = Order::with([
    'customer',
    'customer.address',
    'items.product'
])->get();

// Outcome
query_count = reduced;
database_load = optimized;
page_response_time = improved;

How to find your N+1 problems: install Laravel Debugbar in your development environment. It shows every query run on each page load with the stack trace that triggered it. If you see the same query running dozens of times with slightly different IDs, you have an N+1 problem. Fix it with with() for eager loading.

For production monitoring, Telescope or a service like Clockwork will show you query counts per request in production. Any endpoint running more than 20–30 queries is worth investigating. Most well-tuned Laravel apps run 5–15 queries per page request.

— Problem 2 — Missing Database Indexes
A query that takes 2ms on a small table can take 4 seconds on a large one.

As your database grows, queries that ran quickly on a table with 10,000 rows become slow on a table with 5 million rows — unless the columns being queried are indexed. Laravel's Eloquent makes it easy to write queries without thinking about indexes, which is fine during development and becomes a serious problem at scale.

How to find missing indexes — EXPLAIN your slow queries
-- Run this in MySQL for any slow query
EXPLAIN SELECT * FROM orders
  WHERE status = 'pending'
  AND created_at > '2024-01-01'
  ORDER BY created_at DESC;

-- Look at the 'type' column in the result:
-- 'ALL' = full table scan = BAD (no index being used)
-- 'ref' or 'range' = index being used = GOOD

-- Fix: add an index in a Laravel migration
Schema::table('orders', function (Blueprint $table) {
    $table->index(['status', 'created_at']);  // composite index
});

The columns to index first: any column you filter by in a WHERE clause (status, user_id, created_at). Any foreign key column. Any column you ORDER BY on large result sets. Composite indexes — indexes on two or three columns together — are often more effective than individual column indexes when you consistently filter by multiple columns.

What to be careful of: indexes speed up reads but slow down writes. A table with 20 indexes on it has 20 things to update every time a row is inserted or modified. On a high-write table like a job queue or an event log, too many indexes can actually hurt performance. The right balance depends on your read/write ratio.

— Problem 3 — No Caching Layer
Hitting the database for data that doesn't change frequently is expensive at scale.

At 10,000 active users, your database is handling concurrent requests from every one of them. Many of those requests are asking for the same data — configuration values, product lists, category trees, lookup tables — that doesn't change more than a few times a day. Without caching, every request hits the database. With Redis caching, the first request hits the database and stores the result; the next 9,999 requests read from memory.

Laravel Redis caching — the patterns that matter
// Cache a result for 60 minutes
$categories = Cache::remember('product_categories', 3600, function () {
    return Category::with('children')->active()->get();
});

// Cache with tags (allows targeted invalidation)
$product = Cache::tags(['products', 'product-'.$id])
    ->remember('product-'.$id, 3600, function () use ($id) {
        return Product::with(['variants', 'images'])->find($id);
    });

// Invalidate when product is updated
Cache::tags(['product-'.$product->id])->flush();

// Route-level caching for public pages
// In routes/web.php — cache the response for 5 minutes
Route::get('/products', [ProductController::class, 'index'])
    ->middleware('cache.headers:public;max_age=300');

The most common caching mistake: caching too broadly. Caching a page that includes user-specific data (cart count, account status) means every user sees the cached version — usually the first user's data. Always cache at the level of the data, not the page, when the page contains any user-specific content.

Which driver to use: Redis is the right choice for production. It supports cache tags (which allow targeted invalidation), pub/sub for broadcasting, and is significantly faster than file or database caching. Set up a dedicated Redis instance if you're not already running one — it's the single highest-ROI infrastructure addition for a scaling Laravel app.

— Problem 4 — Synchronous Jobs That Should Be Queued
Anything that takes more than 200ms and doesn't need an immediate result belongs in a queue.

A common pattern in early-stage Laravel apps: the user submits a form, the controller processes the request, sends a confirmation email, updates the CRM, generates a PDF, and only then returns the response. The user waits for all of this. At low traffic this is fine — the total time is 800ms, nobody complains. At high traffic, the queue of users waiting for their email to send before they get a response builds up, and response times become seconds.

Move slow operations to queues
// SYNCHRONOUS — user waits for all of this
public function store(Request $request) {
    $order = Order::create($request->validated());
    Mail::to($order->customer)->send(new OrderConfirmation($order));  // ~400ms
    $this->crm->createContact($order->customer);                     // ~600ms
    $this->pdf->generateReceipt($order);                             // ~800ms
    return response()->json(['order_id' => $order->id]);
}

// QUEUED — user gets response immediately
public function store(Request $request) {
    $order = Order::create($request->validated());
    SendOrderConfirmation::dispatch($order);      // ~5ms — queued
    SyncOrderToCRM::dispatch($order);             // ~5ms — queued
    GenerateOrderReceipt::dispatch($order);       // ~5ms — queued
    return response()->json(['order_id' => $order->id]);
    // Total response time: ~50ms instead of ~1800ms
}

Which queue driver to use: Redis is again the right choice for production. Database queues work but create a high volume of read/write operations on your main database under load — the opposite of what you want when you're trying to reduce database load. Redis queues are fast, reliable, and separate from your application database.

Horizon for queue monitoring: if you're running queues in production, Laravel Horizon is worth installing. It gives you a real-time dashboard of queue throughput, failed jobs, and wait times. Failed job monitoring is particularly important — a job that's silently failing means emails aren't being sent, CRM updates aren't happening, or PDFs aren't being generated. You want to know about this.

— Problem 5 — Laravel Config and Route Caching Not Enabled
Three Laravel cache commands that should be running on every production deploy.

This is the quickest win in this list. Laravel boots by reading and parsing your config files, route definitions, and service provider registrations on every request — unless you've cached them. In production, your config and routes don't change between requests. Caching them means this work is done once at deploy time, not on every request.

The three commands to add to your deployment script
# Run these at the end of every production deploy
php artisan config:cache     # Caches all config files into one file
php artisan route:cache      # Caches all route definitions
php artisan view:cache       # Pre-compiles all Blade templates

# And at the start of deploy (to clear stale caches)
php artisan optimize:clear

# Or use the single optimise command (Laravel 10+)
php artisan optimize         # Runs config:cache, route:cache, view:cache

# Important: never run config:cache in local development
# It bakes in the config values at cache time — .env changes won't work
# until you clear the cache

The performance impact depends on your application size. For a medium-size Laravel app (50+ routes, 20+ config files), route and config caching typically reduces per-request overhead by 5–15ms. That might not sound like much, but at high concurrency it compounds — and it's a two-minute change to your deployment script.

— Problem 6 — OPcache Not Configured Correctly
PHP's built-in performance tool that many apps aren't using properly.

PHP OPcache compiles PHP files to bytecode and stores them in shared memory. Without it, PHP compiles your application's files from source on every request. With it, the compilation happens once and subsequent requests use the cached bytecode. For a Laravel application, this is a significant difference.

OPcache is enabled in most production PHP installations, but the default configuration isn't always optimal for a Laravel app. The key settings to check and adjust:

OPcache settings for Laravel production (php.ini)
opcache.enable=1
opcache.memory_consumption=256          ; Increase from default 128
opcache.max_accelerated_files=20000     ; Increase from default 10000
                                        ; Check actual file count:
                                        ; find /path/to/app -name '*.php' | wc -l
opcache.validate_timestamps=0           ; CRITICAL for production performance
                                        ; Don't check if files changed on disk
                                        ; Requires opcache reset on deploy
opcache.revalidate_freq=0               ; With validate_timestamps=0, this is moot

# Add to your deploy script to reset OPcache after deploy:
php artisan opcache:clear               ; (requires Laravel OPcache package)
# or:
kill -USR2 $(cat /var/run/php-fpm.pid)  ; Graceful PHP-FPM reload

The performance impact depends on your application size. For a medium-size Laravel app (50+ routes, 20+ config files), route and config caching typically reduces per-request overhead by 5–15ms. That might not sound like much, but at high concurrency it compounds — and it's a two-minute change to your deployment script.

- When These Fixes Aren't Enough

The signs that you need an architectural conversation, not just a tuning session.

The fixes above cover the majority of Laravel performance problems at the 10,000–50,000 active user scale. If you've addressed all five and you're still seeing slow response times under load, the conversation shifts to architecture.

01

Database read replicas

If your application is read-heavy (most are), a read replica takes the SELECT query load off your primary database. Laravel supports read/write connections natively — you define a read host and a write host in config/database.php and Laravel routes accordingly.

02

Horizontal scaling and load balancing

If you're on a single server and adding a caching layer hasn't solved the problem, you need multiple application servers behind a load balancer. This requires your session storage to be centralised (Redis, not files), your file storage to be centralised (S3 or equivalent, not local disk), and your queue workers to be aware of the distributed environment.

03

Full-text search off the database

If search is a core feature and users are complaining about search performance specifically, MySQL LIKE queries don't scale. Adding Typesense or Meilisearch takes search load off the database entirely and gives you sub-100ms results with typo tolerance. We've made this switch on several applications — it's a contained change that solves a specific problem.

-FAQ

Questions about Laravel performance at scale

Laravel Telescope in development — it shows queries, requests, jobs, and exceptions with full stack traces. For production, Clockwork or a service like Blackfire.io gives you request profiling without the overhead of Telescope. Start by looking at endpoints with the highest query counts (N+1 candidates) and the slowest average response times. Fix the highest-impact issues first.
For most Laravel applications, the database engine choice matters less than indexes, query design, and caching. MySQL and PostgreSQL both perform well at the scale where most Laravel apps run into issues. PostgreSQL has better support for complex queries and JSON columns, which can matter for specific use cases. But switching databases mid-project is a significant migration with real risks — fix your queries and caching before considering it.
Some of it, yes — N+1 fixes, query indexes, and config caching will help on any infrastructure. But if you're on shared hosting and dealing with serious traffic, the infrastructure is the constraint. OPcache is often disabled or limited on shared hosting. Redis is often unavailable. Moving to a VPS or a managed cloud provider (DigitalOcean, Render, AWS) is usually the highest-impact change for an application that's outgrown shared hosting.
Fixing a significant N+1 problem on a high-traffic endpoint can reduce its query count from 100+ to 2–3 and its response time from 2–3 seconds to under 200ms. Adding Redis caching to frequently-accessed data can reduce database queries by 70–80% for read-heavy pages. Config and route caching saves 5–15ms per request. The improvements compound — all five fixes together can take an application that was struggling at 5,000 concurrent users to handling 30,000+ comfortably.

Is your Laravel application slowing down under load?

We've diagnosed and fixed performance problems on Laravel applications ranging from startups hitting their first scaling wall to enterprise systems with millions of records. Start with a performance audit — we'll tell you exactly what's causing the slowdown and what it will take to fix it.

Recent Posts

  • Articles
  • Dot Net

We Migrated a 12-Year-Old .NET Framework App to .NET 8. Here’s What We Found Inside

PROJECT DIARY ✦ .NET MODERNISATION ✦ LEGACY TO .NET 8 11 min read · 2026…

1 hour ago
  • Guide
  • Offshore Development

Offshore Development Doesn’t Work — Unless You Do These 5 Things

GUIDE ✦ OFFSHORE DEVELOPMENT ✦ UK · US · AUSTRALIAs 8 min read · 2026…

1 hour ago
  • Guide

Reduce Weekly Reporting Time from 3 Hours to 3 Minutes with Power BI

GUIDE ✦ OFFSHORE DEVELOPMENT ✦ UK · US · AUSTRALIAs 8 min read · 2026…

5 hours ago
  • Articles
  • Dot Net

Seamless Migration from .NET 4.5 to .NET 8 with Zero Downtime

⚙️ Legacy System Modernization 🏦 Finance Industry 🇬🇧 United Kingdom 12-Year-Old Platform Seamless Migration from…

5 hours ago
back to top