Moniruzzaman Saikat

Posted Sep 29, 2026 · 4 min read · 0 views

Report

Speeding Up a Slow Laravel App: Caching, Queries, and Queues

Every Laravel app feels fast on day one. Then the data grows, traffic picks up, and pages that loaded in 200ms start taking 3 seconds. The good news is that most slow Laravel apps suffer from the same three problems: too many database queries, too little caching, and too much work done during the request.

In this post we will fix all three, with code you can use today.

Step 1: Measure before you optimize

Guessing is the fastest way to waste an afternoon. Install Laravel Debugbar in your local environment:

composer require barryvdh/laravel-debugbar --dev

Open any page and check the Queries tab. You will see how many queries ran and how long each took. For production, Laravel Telescope or a tool like Sentry Performance gives you similar data.

Your goal is simple: find the slowest pages, then find out whether the time goes to queries, external API calls, or heavy processing.

Step 2: Kill the N+1 query problem

This is the number one cause of slow Laravel pages. Look at this code:

$posts = Post::all();

foreach ($posts as $post) {
    echo $post->author->name;
}

If you have 100 posts, this runs 101 queries: one for the posts and one per post for the author. Fix it with eager loading:

$posts = Post::with('author')->get();

Now it runs just 2 queries, no matter how many posts you have.

You can also make Laravel yell at you when you forget. Add this to AppServiceProvider::boot():

use Illuminate\Database\Eloquent\Model;

Model::preventLazyLoading(! app()->isProduction());

In local and staging, any lazy loaded relation will now throw an exception, so you catch N+1 problems before they ship.

Select only what you need

Loading every column is wasteful, especially with large text fields:

$users = User::select('id', 'name', 'email')->paginate(20);

Also prefer paginate() over get() for lists, and use chunk() or cursor() when processing thousands of rows so you do not exhaust memory.

Add indexes

If you filter or sort by a column, it probably needs an index:

Schema::table('orders', function (Blueprint $table) {
    $table->index(['user_id', 'status']);
});

Use EXPLAIN on your slow queries to confirm the database is actually using the index.

Step 3: Cache what does not change often

Once your queries are lean, stop running them repeatedly. Laravel makes caching easy with Cache::remember:

$categories = Cache::remember('categories.all', 3600, function () {
    return Category::orderBy('name')->get();
});

The first request runs the query and stores the result for one hour. Every request after that reads from the cache.

Invalidate when data changes

Stale cache causes confusing bugs. Clear the key when the underlying data changes, using a model observer or the saved and deleted events:

protected static function booted()
{
    static::saved(fn () => Cache::forget('categories.all'));
    static::deleted(fn () => Cache::forget('categories.all'));
}

Use a real cache driver

The default file driver works, but Redis is much faster and supports tags. Set it in your .env:

CACHE_STORE=redis

Cache config, routes, and views in production

Run these during deployment:

php artisan config:cache
php artisan route:cache
php artisan view:cache
php artisan event:cache

These commands remove a lot of file parsing on every request, and they cost you nothing.

Step 4: Move slow work to queues

Sending emails, generating PDFs, resizing images, and calling third party APIs should not block your user. Push them to a queue.

Instead of this:

Mail::to($user)->send(new WelcomeMail($user));

Do this:

Mail::to($user)->queue(new WelcomeMail($user));

For custom work, create a job:

php artisan make:job GenerateInvoicePdf
class GenerateInvoicePdf implements ShouldQueue
{
    use Queueable;

    public function __construct(public Invoice $invoice) {}

    public function handle(): void
    {
        // Heavy PDF generation here
    }
}

Dispatch it from your controller:

GenerateInvoicePdf::dispatch($invoice);

The user gets an instant response while a worker handles the job in the background. Use Redis as your queue driver and run workers with Supervisor so they restart automatically if they crash:

php artisan queue:work --tries=3 --timeout=90

If you are on shared hosting without Supervisor, you can run php artisan queue:work --stop-when-empty from a cron job every minute as a workable fallback.

Step 5: Quick wins you should not skip

  • Enable OPcache on your server. It caches compiled PHP and gives a free speed boost.
  • Use the latest stable PHP version. Each release is noticeably faster than the last.
  • Optimize Composer autoloading in production with composer install --optimize-autoloader --no-dev.
  • Serve assets through a CDN and compress them with Vite's production build.
  • Avoid heavy logic in Blade views. Prepare data in controllers or view models instead.

A simple checklist

Before you call your app "optimized", verify these:

  1. No N+1 queries on your top 10 pages
  2. Indexes on all filtered and sorted columns
  3. Frequently read, rarely changed data is cached
  4. Emails, exports, and API calls run in queues
  5. Config, route, and view caches are enabled in production

Final thoughts

Performance work is not about clever tricks. It is about measuring, finding the real bottleneck, and fixing it with the right tool: eager loading for queries, caching for repeated reads, and queues for slow tasks. Do these three well and most Laravel apps become several times faster without any server upgrade.

Have a favorite Laravel performance tip that I missed? Share it in the comments.

1 reaction
0

Written by

Moniruzzaman Saikat

Software Engineer at TheSoftking Ltd

Software engineer who loves building useful things, solving hard problems, and turning ideas into scalable products. Always learning, shipping, and experimenting with new tech.

Founding MemberNew MemberProlific Writer

14 articles · Dhaka Bangladesh · Joined Sep 2026

Discussion (0)

Sign in to join the discussion.