Every 10x of growth breaks a different part of your app. Here is the order it happens in, and the one fix that matters at each step.
Your Laravel app feels fast with 1,000 users. At 100,000, pages start to crawl. At a million, the database gives up during a traffic spike. The usual reaction is to buy a bigger server, which costs the most and often fixes the least.
💥 Master AI & Tech Skills
💰 One Plan. Multiple Courses. More Savings.
🔥 Get Up to 50% OFF | ⏰ Limited-Time Offer
👉 Enroll Now & Start Learning
Here is what most people miss when they try to scale a Laravel app: every 10x of users breaks a different thing, and it breaks in a clear order. Fix things in that order and each step is cheap. Skip ahead and you pay for complexity you do not need yet.
So here is the order, from 1K users to 10M.
Measure First, Because Guessing Is the Expensive Part
Before you change anything, find out what is slow. Three tools do most of the work:
- The MySQL slow query log shows which queries take too long.
- Laravel Pulse shows your slowest routes, queries, and jobs in production.
- Laravel Telescope (local or staging) shows every query a request runs.
Write down your slowest five routes. Everything below is about making that list shorter.
1K to 10K Users: Fix Your Queries, Not Your Server
At this size, one slow query is almost always the problem. The most common one is the N+1 query: you load a list, then your code runs one extra query for every row.
// Slow: 1 query for orders + 1 more query for each order's customer
$orders = Order::latest()->take(50)->get();
foreach ($orders as $order) {
echo $order->customer->name;
}
// Fast: 2 queries in total, no matter how many orders
$orders = Order::with('customer')->latest()->take(50)->get();Make Laravel shout when you make this mistake. Add this to your AppServiceProvider:
use Illuminate\Database\Eloquent\Model;
public function boot(): void
{
Model::preventLazyLoading(! $this->app->isProduction());
}Then check your indexes. I covered that in Stop Adding MySQL Indexes — Your Slow Laravel Queries Need a Different Fix, so I will not repeat it here. Once the queries are clean, a single small server can often carry this whole stage.
10K to 100K Users: Cache What Many People Read and Few People Change
Now your database does the same work again and again. A home page that shows the top 10 products does not need to ask MySQL on every visit. Use Redis (set CACHE_STORE=redis in your .env; older Laravel versions use CACHE_DRIVER) and cache the answer:
$topProducts = Cache::remember('home:top-products', now()->addMinutes(10), function () {
return Product::query()
->withCount('orders')
->orderByDesc('orders_count')
->limit(10)
->get();
});One call now replaces thousands of identical queries. When the data changes, clear it with Cache::forget('home:top-products');.
Start with the pages that get the most traffic. Do not cache everything. A cache you cannot clear properly shows wrong data, and that bug is harder to find than a slow page.
100K to 1M Users: Move Slow Work Out of the Request
At this size, the work your app does while the user waits becomes the problem. Sending an email, resizing an image, or calling a payment API can take seconds. Under load, those seconds stack up, and requests pile up behind them.
The fix is a queue. Reply to the user first, and do the slow work in the background:
// Slow: the user waits for the mail provider
Mail::to($user)->send(new WelcomeMail($user));
// Fast: the user gets a reply now, and a worker sends the email
Mail::to($user)->queue(new WelcomeMail($user));Set QUEUE_CONNECTION=redis, then run workers with php artisan queue:work redis --tries=3. Laravel Horizon gives you a dashboard to watch them.
Two warnings. First, jobs fail, so you must watch them. I wrote about why the failed jobs table can mislead you in Your Laravel Failed Jobs Table Is Lying to You — Here’s the Real Fix. Second, jobs can run twice, so make every job safe to repeat. The duplicate payment lesson in A Redis Lock Alone Won’t Stop Duplicate Payments in Laravel applies here too.
1M to 10M Users: Split Reads From Writes, Then Add Servers
Most apps read data far more often than they write it. So the next step is to send reads to copies of your database. Laravel supports this out of the box:
'mysql' => [
'read' => [
'host' => [env('DB_READ_HOST_1'), env('DB_READ_HOST_2')],
],
'write' => [
'host' => [env('DB_WRITE_HOST')],
],
'sticky' => true,
'driver' => 'mysql',
'database' => env('DB_DATABASE'),
// username, password, charset and the rest stay the same
],The sticky option matters. After a request writes data, Laravel reads from the main database for the rest of that request, so a user does not save something and then fail to see it.
Next, make your app servers stateless, so you can put more of them behind a load balancer. Three changes do it: sessions in Redis (SESSION_DRIVER=redis), uploaded files on S3 (FILESYSTEM_DISK=s3), and the cache in Redis. When no server holds anything the others need, adding a server is just turning one on.
Last, protect yourself from traffic spikes and bad actors with a rate limit:
// In AppServiceProvider::boot()
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});The Fixes You Can Safely Skip for Now
Notice what is not on the list: sharding, microservices, Kubernetes. They are real tools, but most apps reach a million users without them. I argued this in Your App Doesn’t Need Sharding Yet — Here’s What It Needs First, and I wrote about what broke when I split an app too early in I Split My Laravel App Into Microservices. Here’s What Broke in Production. Each of these adds more moving parts, and every part can fail on its own.
Add them when your measurements say the simple steps are used up. Do not add them because a blog post, including this one, made you nervous.
Where These Numbers Bend
The user counts above are rough guides, not rules. What matters is requests per second and what each request does, not how many people signed up.
- An app with heavy reports or file uploads hits these walls sooner.
- A read-heavy site with simple pages can go much further on one server.
- Read replicas can lag by a moment, so avoid them for data that must be perfectly fresh, like an account balance.
- Every cache needs a clear plan for when the data changes.
Test before you trust. A load testing tool such as k6 can show you your real limit in an afternoon.
What To Do Before the Next 10x
Find the stage you are in today and do only that step. Measure again. Then go back to shipping features until the numbers say it is time for the next step.
You do not need a bigger server. You need to know which wall you are about to hit.
If you want a clean starting point for the API side of a Laravel project, I am putting my Laravel API starter kit together for Gumroad.
Комментарии (0)
Пока нет комментариев — будьте первым.