Your Laravel application is taking 5 seconds to load. Your users are frustrated. Your bounce rate is through the roof. Sound familiar?
We’ve helped companies reduce their Laravel response times from 2000ms to under 50ms. The secret isn’t one silver bullet - it’s a systematic approach to identifying and eliminating performance bottlenecks at every level of your application.
This guide reveals the exact techniques we use to achieve 10x performance improvements in production Laravel applications serving millions of requests daily.
The Real Cost of Poor Performance
Before diving into optimizations, let’s quantify what slow performance actually costs:
- Amazon loses 1% of sales for every 100ms of latency
- Google found that a 500ms delay in search results caused a 20% drop in traffic
- Walmart saw a 2% increase in conversions for every 1 second of improvement
For a typical e-commerce site making $100,000/month, a 1-second improvement could mean an extra $24,000 in annual revenue. That’s why performance optimization isn’t just a technical exercise - it’s a business imperative.
Performance Profiling: Measure Before You Optimize
You can’t optimize what you can’t measure. Start by establishing a baseline:
Setting Up Laravel Debugbar
composer require barryvdh/laravel-debugbar --dev
// config/debugbar.php
'enabled' => env('DEBUGBAR_ENABLED', false),
'collectors' => [
'queries' => true,
'time' => true,
'memory' => true,
'views' => true,
'cache' => true,
],
Laravel Telescope for Production Monitoring
composer require laravel/telescope
php artisan telescope:install
php artisan migrate
// app/Providers/TelescopeServiceProvider.php
public function register()
{
Telescope::night();
$this->hideSensitiveRequestDetails();
Telescope::filter(function (IncomingEntry $entry) {
if ($this->app->environment('local')) {
return true;
}
return $entry->isReportableException() ||
$entry->isFailedRequest() ||
$entry->isFailedJob() ||
$entry->isScheduledTask() ||
$entry->hasMonitoredTag();
});
}
Creating Performance Benchmarks
// app/Services/PerformanceBenchmark.php
namespace App\Services;
use Illuminate\Support\Facades\Log;
class PerformanceBenchmark
{
private $checkpoints = [];
private $startTime;
public function start()
{
$this->startTime = microtime(true);
$this->checkpoints = [];
}
public function checkpoint($name)
{
$this->checkpoints[$name] = microtime(true) - $this->startTime;
}
public function end($label = 'Performance')
{
$totalTime = microtime(true) - $this->startTime;
Log::info($label, [
'total_time' => $totalTime,
'checkpoints' => $this->checkpoints,
'memory_peak' => memory_get_peak_usage(true) / 1024 / 1024 . ' MB',
]);
return $totalTime;
}
}
Database Optimization: The #1 Performance Killer
Database queries are responsible for 80% of performance issues in Laravel applications. Here’s how to fix them:
Solving N+1 Query Problems
The N+1 problem is Laravel’s most common performance killer:
// ❌ BAD: N+1 Problem (1 query for posts + N queries for authors)
$posts = Post::all(); // Query 1
foreach ($posts as $post) {
echo $post->author->name; // Query 2, 3, 4... N+1
}
// ✅ GOOD: Eager Loading (2 queries total)
$posts = Post::with('author')->get();
foreach ($posts as $post) {
echo $post->author->name; // No additional queries
}
// ✅ BETTER: Eager Loading Multiple Relations
$posts = Post::with(['author', 'comments', 'tags'])->get();
// ✅ BEST: Nested Eager Loading
$posts = Post::with([
'author.profile',
'comments' => function ($query) {
$query->latest()->limit(5);
}
])->get();
Query Optimization Techniques
// 1. Select Only What You Need
// ❌ BAD
$users = User::all(); // Selects all columns
// ✅ GOOD
$users = User::select('id', 'name', 'email')->get();
// 2. Use Chunks for Large Datasets
// ❌ BAD
$users = User::all(); // Loads everything into memory
foreach ($users as $user) {
// Process
}
// ✅ GOOD
User::chunk(1000, function ($users) {
foreach ($users as $user) {
// Process in batches
}
});
// 3. Use Cursor for Memory Efficiency
// ✅ BEST for large datasets
foreach (User::cursor() as $user) {
// Process one at a time, minimal memory usage
}
// 4. Optimize Aggregations
// ❌ BAD
$count = User::all()->count(); // Loads all records then counts
// ✅ GOOD
$count = User::count(); // COUNT query
// 5. Use Raw Queries for Complex Operations
$topProducts = DB::select(DB::raw("
SELECT p.*,
COUNT(o.id) as order_count,
SUM(oi.quantity) as total_sold
FROM products p
JOIN order_items oi ON p.id = oi.product_id
JOIN orders o ON oi.order_id = o.id
WHERE o.created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)
GROUP BY p.id
ORDER BY total_sold DESC
LIMIT 10
"));
Database Indexing Strategy
Proper indexing can improve query performance by 100x:
// Migration for optimal indexing
Schema::table('posts', function (Blueprint $table) {
// Single column indexes
$table->index('user_id');
$table->index('status');
$table->index('published_at');
// Composite indexes (order matters!)
$table->index(['status', 'published_at']);
$table->index(['user_id', 'status', 'published_at']);
// Unique indexes are faster
$table->unique('slug');
// Full-text search index
$table->fullText(['title', 'content']);
});
// Using indexes effectively in queries
Post::where('status', 'published')
->where('published_at', '<=', now())
->where('user_id', $userId)
->get(); // Uses composite index
Query Result Caching
// Simple query caching
$users = Cache::remember('active-users', 3600, function () {
return User::where('active', true)
->with('profile')
->get();
});
// Tagged cache for easy invalidation
$products = Cache::tags(['products', 'inventory'])->remember(
"products-page-{$page}",
3600,
function () use ($page) {
return Product::paginate(20, ['*'], 'page', $page);
}
);
// Invalidate when updating
Cache::tags('products')->flush();
Caching Strategies That Actually Work
Multi-Layer Caching Architecture
// config/cache.php
'stores' => [
'redis_fast' => [ // For hot data
'driver' => 'redis',
'connection' => 'cache_fast',
],
'redis_slow' => [ // For cold data
'driver' => 'redis',
'connection' => 'cache_slow',
],
],
// Usage
class ProductRepository
{
public function find($id)
{
// L1 Cache: In-memory
static $memory = [];
if (isset($memory[$id])) {
return $memory[$id];
}
// L2 Cache: Redis hot data (5 minutes)
$product = Cache::store('redis_fast')->remember(
"product:{$id}",
300,
function () use ($id) {
// L3 Cache: Redis cold data (1 hour)
return Cache::store('redis_slow')->remember(
"product:full:{$id}",
3600,
function () use ($id) {
// L4: Database
return Product::with('images', 'reviews')->find($id);
}
);
}
);
$memory[$id] = $product;
return $product;
}
}
Route and Config Caching
# Production deployment script
php artisan config:cache # Cache configuration
php artisan route:cache # Cache routes
php artisan view:cache # Cache views
php artisan event:cache # Cache events and listeners
# Combined optimization
php artisan optimize
# Clear all caches
php artisan optimize:clear
Full Page Caching
// app/Http/Middleware/CacheResponse.php
namespace App\Http\Middleware;
use Closure;
use Cache;
class CacheResponse
{
public function handle($request, Closure $next)
{
// Skip for authenticated users
if (auth()->check()) {
return $next($request);
}
$key = 'page:' . sha1($request->fullUrl());
return Cache::remember($key, 600, function () use ($request, $next) {
return $next($request);
});
}
}
// Apply to routes
Route::middleware(['cache.response'])->group(function () {
Route::get('/', 'HomeController@index');
Route::get('/products', 'ProductController@index');
});
Queue Optimization for Background Processing
Move heavy operations to background queues:
// ❌ BAD: Synchronous processing
public function store(Request $request)
{
$order = Order::create($request->validated());
// These block the response
$this->sendOrderEmail($order);
$this->generateInvoice($order);
$this->updateInventory($order);
$this->notifyWarehouse($order);
return response()->json($order);
}
// ✅ GOOD: Asynchronous processing
public function store(Request $request)
{
$order = Order::create($request->validated());
// Dispatch to queues
ProcessNewOrder::dispatch($order);
SendOrderConfirmation::dispatch($order)->onQueue('emails');
GenerateInvoice::dispatch($order)->onQueue('documents')->delay(now()->addMinutes(5));
return response()->json($order);
}
// Job implementation
class ProcessNewOrder implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $order;
public $tries = 3;
public $timeout = 120;
public function __construct(Order $order)
{
$this->order = $order;
}
public function handle()
{
$this->order->updateInventory();
$this->order->notifyWarehouse();
// Chain next job
GenerateShippingLabel::dispatch($this->order);
}
public function failed(Throwable $exception)
{
// Notify admin of failure
Log::error('Order processing failed', [
'order_id' => $this->order->id,
'error' => $exception->getMessage()
]);
}
}
Queue Worker Optimization
# Optimized queue worker command
php artisan queue:work redis \
--queue=critical,high,default,low \
--sleep=3 \
--tries=3 \
--timeout=90 \
--memory=512 \
--max-jobs=1000 \
--max-time=3600
Code-Level Optimizations
Service Container Optimization
// ❌ BAD: Binding in every request
public function index()
{
app()->bind(PaymentGateway::class, function () {
return new StripeGateway(config('stripe.key'));
});
$gateway = app(PaymentGateway::class);
}
// ✅ GOOD: Singleton binding in service provider
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(PaymentGateway::class, function ($app) {
return new StripeGateway(config('stripe.key'));
});
}
Collection Optimization
// ❌ BAD: Multiple iterations
$users = User::all();
$activeUsers = $users->filter(fn($user) => $user->active);
$verifiedUsers = $activeUsers->filter(fn($user) => $user->verified);
$names = $verifiedUsers->pluck('name');
// ✅ GOOD: Single iteration with lazy collection
$names = User::cursor()
->filter(fn($user) => $user->active && $user->verified)
->pluck('name');
// ✅ BETTER: Database-level filtering
$names = User::where('active', true)
->where('verified', true)
->pluck('name');
Lazy Loading Prevention
// app/Providers/AppServiceProvider.php
use Illuminate\Database\Eloquent\Model;
public function boot()
{
// Prevent lazy loading in non-production
Model::preventLazyLoading(!app()->isProduction());
// Or throw exceptions
Model::handleLazyLoadingViolationUsing(function ($model, $relation) {
Log::warning("Lazy loading detected: {$model}::{$relation}");
});
}
API Response Optimization
Efficient Resource Serialization
// ❌ BAD: Loading everything
return User::with(['posts', 'comments', 'likes', 'followers'])->get();
// ✅ GOOD: API Resources with selective loading
class UserResource extends JsonResource
{
public function toArray($request)
{
return [
'id' => $this->id,
'name' => $this->name,
'email' => $this->when($request->user()->isAdmin(), $this->email),
'posts_count' => $this->whenLoaded('posts', fn() => $this->posts->count()),
'latest_posts' => PostResource::collection(
$this->whenLoaded('posts', fn() => $this->posts->take(5))
),
];
}
}
// Controller
public function index(Request $request)
{
$users = User::query()
->when($request->include === 'posts', fn($q) => $q->with('posts'))
->paginate();
return UserResource::collection($users);
}
Response Compression
// app/Http/Middleware/CompressResponse.php
namespace App\Http\Middleware;
class CompressResponse
{
public function handle($request, Closure $next)
{
$response = $next($request);
if ($this->shouldCompress($request, $response)) {
$content = $response->getContent();
$compressed = gzencode($content, 9);
$response->setContent($compressed);
$response->headers->add([
'Content-Encoding' => 'gzip',
'Vary' => 'Accept-Encoding',
'Content-Length' => strlen($compressed),
]);
}
return $response;
}
private function shouldCompress($request, $response)
{
return $response->getStatusCode() === 200
&& strpos($request->header('Accept-Encoding'), 'gzip') !== false
&& strlen($response->getContent()) > 1024;
}
}
Asset Optimization
Laravel Mix Optimization
// webpack.mix.js
const mix = require('laravel-mix');
mix.js('resources/js/app.js', 'public/js')
.vue()
.sass('resources/sass/app.scss', 'public/css')
.version()
.extract(['vue', 'axios', 'lodash']) // Extract vendor libraries
.options({
terser: {
extractComments: false,
terserOptions: {
compress: {
drop_console: true,
},
},
},
})
.webpackConfig({
output: {
chunkFilename: 'js/chunks/[name].[chunkhash].js',
},
});
// Production build
if (mix.inProduction()) {
mix.options({
cssNano: {
discardComments: {removeAll: true},
}
})
.purgeCss({
enabled: true,
globs: [
path.join(__dirname, 'resources/views/**/*.blade.php'),
path.join(__dirname, 'resources/js/**/*.vue'),
],
});
}
Image Optimization
// app/Services/ImageOptimizer.php
namespace App\Services;
use Intervention\Image\Facades\Image;
use Illuminate\Support\Facades\Storage;
class ImageOptimizer
{
public function optimize($path, $maxWidth = 1920)
{
$image = Image::make(Storage::path($path));
// Resize if larger than max width
if ($image->width() > $maxWidth) {
$image->resize($maxWidth, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
}
// Generate responsive versions
$sizes = [
'thumbnail' => 150,
'small' => 300,
'medium' => 600,
'large' => 1200,
];
foreach ($sizes as $name => $width) {
$resized = clone $image;
$resized->resize($width, null, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
$newPath = str_replace('.', "-{$name}.", $path);
Storage::put($newPath, $resized->encode('jpg', 85));
}
// Save optimized original
$image->save(Storage::path($path), 85);
return true;
}
}
Infrastructure Optimization
PHP-FPM Tuning
; /etc/php/8.2/fpm/pool.d/www.conf
[www]
; Dynamic process management
pm = dynamic
pm.max_children = 50
pm.start_servers = 10
pm.min_spare_servers = 5
pm.max_spare_servers = 20
pm.max_requests = 1000
; Keep-alive
pm.process_idle_timeout = 10s
request_terminate_timeout = 30s
; Status monitoring
pm.status_path = /fpm-status
ping.path = /fpm-ping
; Slow log
slowlog = /var/log/php8.2-fpm-slow.log
request_slowlog_timeout = 5s
OPcache Configuration
; /etc/php/8.2/mods-available/opcache.ini
opcache.enable=1
opcache.enable_cli=0
opcache.memory_consumption=512
opcache.interned_strings_buffer=64
opcache.max_accelerated_files=20000
opcache.max_wasted_percentage=10
opcache.validate_timestamps=0
opcache.revalidate_freq=0
opcache.save_comments=1
opcache.enable_file_override=1
opcache.optimization_level=0x7FFEBFFF
opcache.lockfile_path=/tmp
opcache.file_cache=/tmp/opcache
opcache.huge_code_pages=1
Redis Optimization
# /etc/redis/redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru
save "" # Disable persistence for cache-only Redis
tcp-keepalive 60
timeout 300
tcp-backlog 511
# Enable compression
# For Laravel cache
rdbcompression yes
Real-World Case Study: From 2s to 50ms
Let’s examine a real e-commerce application optimization:
Before Optimization (2000ms average response time)
// HomeController.php - BEFORE
public function index()
{
$products = Product::all();
$categories = Category::all();
$featuredProducts = Product::where('featured', true)->get();
foreach ($products as $product) {
$product->review_count = $product->reviews->count();
$product->average_rating = $product->reviews->avg('rating');
}
return view('home', compact('products', 'categories', 'featuredProducts'));
}
After Optimization (50ms average response time)
// HomeController.php - AFTER
public function index()
{
$data = Cache::remember('home:page:data', 3600, function () {
return [
'products' => Product::select('id', 'name', 'slug', 'price', 'image')
->withCount('reviews')
->withAvg('reviews', 'rating')
->limit(12)
->get(),
'categories' => Category::select('id', 'name', 'slug')
->withCount('products')
->get(),
'featuredProducts' => Product::select('id', 'name', 'slug', 'price', 'image')
->where('featured', true)
->with('reviews:product_id,rating')
->limit(4)
->get(),
];
});
return view('home', $data);
}
Optimization Results
- Database queries: Reduced from 245 to 3
- Memory usage: Reduced from 128MB to 12MB
- Response time: Reduced from 2000ms to 50ms
- Throughput: Increased from 50 req/s to 2000 req/s
Monitoring and Maintaining Performance
Custom Performance Monitoring
// app/Http/Middleware/PerformanceMonitor.php
namespace App\Http\Middleware;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\DB;
class PerformanceMonitor
{
public function handle($request, Closure $next)
{
$startTime = microtime(true);
$startMemory = memory_get_usage();
DB::enableQueryLog();
$response = $next($request);
$duration = microtime(true) - $startTime;
$memory = memory_get_usage() - $startMemory;
$queries = count(DB::getQueryLog());
if ($duration > 1.0) { // Log slow requests
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'duration' => $duration,
'memory' => $memory / 1024 / 1024 . ' MB',
'queries' => $queries,
'query_log' => DB::getQueryLog(),
]);
}
$response->headers->set('X-Response-Time', $duration);
$response->headers->set('X-Query-Count', $queries);
return $response;
}
}
Performance Regression Testing
// tests/Feature/PerformanceTest.php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Support\Facades\DB;
class PerformanceTest extends TestCase
{
public function test_homepage_loads_under_100ms()
{
$start = microtime(true);
$response = $this->get('/');
$duration = microtime(true) - $start;
$this->assertLessThan(0.1, $duration, 'Homepage took longer than 100ms');
$response->assertOk();
}
public function test_product_listing_uses_fewer_than_10_queries()
{
DB::enableQueryLog();
$this->get('/products');
$queryCount = count(DB::getQueryLog());
$this->assertLessThan(10, $queryCount, "Product listing used {$queryCount} queries");
}
}
Common Performance Pitfalls to Avoid
1. Over-Optimization
Don’t optimize prematurely. Profile first, optimize the bottlenecks, ignore the rest.
2. Cache Invalidation Bugs
// Always use tagged cache for related data
Cache::tags(['products', 'inventory'])->flush(); // Clear all product-related caches
3. Memory Leaks in Long-Running Processes
// Clear memory in queue workers
public function handle()
{
// Process job
// Clear Eloquent cache
Model::clearBootedModels();
// Garbage collection
gc_collect_cycles();
}
Performance Optimization Checklist
Before deploying to production, ensure you’ve:
- Enabled OPcache with proper configuration
- Configured Redis for caching and sessions
- Implemented query result caching
- Fixed all N+1 query problems
- Added proper database indexes
- Moved heavy operations to queues
- Optimized and minified assets
- Enabled Gzip compression
- Configured CDN for static assets
- Set up monitoring and alerting
- Implemented health checks
- Tested under load conditions
The Path Forward
Performance optimization is an ongoing process, not a one-time task. As your application grows, new bottlenecks will emerge. The key is to have monitoring in place to catch them early and a systematic approach to addressing them.
Modern deployment tools and automation scripts can handle many of these optimizations automatically - from OPcache configuration to Redis setup to queue worker management. By implementing the techniques in this guide, you can focus on your application logic while ensuring optimal performance across your infrastructure.
Conclusion
We’ve covered the essential techniques for making Laravel applications blazingly fast:
- Profile first - Measure before optimizing
- Fix database queries - They’re usually the biggest bottleneck
- Implement smart caching - Multi-layer caching for maximum efficiency
- Use queues - Move heavy operations to the background
- Optimize infrastructure - Proper PHP-FPM and OPcache configuration
- Monitor continuously - Catch performance regressions early
Apply these techniques systematically, and you’ll see dramatic improvements in your application’s performance. Remember, users expect pages to load in under 2 seconds - with these optimizations, you can deliver responses in under 100ms.
By implementing these performance optimization techniques on your Ubuntu servers, you can achieve enterprise-grade Laravel performance without the complexity of managed platforms, while maintaining full control over your infrastructure and optimization strategies.