Performance can make or break your Laravel application. Over time, I've learned that good performance isn't just about fast code - it's about smart architecture, efficient queries, and strategic caching. Here are some patterns that have worked well for keeping Laravel apps running smoothly.
Performance optimization should be data-driven. Always measure before and after changes using real production data.
The Performance Pyramid
Database
60-80% of performance issues start here
Caching
Fastest way to improve response times
Code
Optimize algorithms and logic
Infrastructure
Scale when optimization isn't enough
Database Performance Mastery
1. Eliminate N+1 Queries
The most common performance killer in Laravel applications:
❌ Problematic Code
// This generates N+1 queries!
$posts = Post::all();
foreach ($posts as $post) {
echo $post->category->name; // Query for each post
echo $post->author->name; // Another query for each post
}
✅ Optimized Code
// This generates only 3 queries total
$posts = Post::with(['category', 'author'])->get();
foreach ($posts as $post) {
echo $post->category->name; // No additional query
echo $post->author->name; // No additional query
}
2. Strategic Database Indexing
Proper indexing can turn slow queries into lightning-fast ones:
<?php
// Migration with performance-focused indexes
Schema::create('blog_posts', function (Blueprint $table) {
$table->id();
$table->string('title');
$table->string('slug')->unique();
$table->text('content');
$table->enum('status', ['draft', 'published']);
$table->timestamp('published_at')->nullable();
$table->foreignId('category_id')->constrained();
$table->foreignId('user_id')->constrained();
$table->timestamps();
// Compound indexes for common query patterns
$table->index(['status', 'published_at']); // For published posts
$table->index(['user_id', 'status']); // For user's posts
$table->index(['category_id', 'status']); // For category filtering
// Full-text search
$table->fullText(['title', 'content']);
});
3. Query Optimization Techniques
Use Select Specific Columns
// Instead of loading all columns
$posts = Post::all();
// Select only what you need
$posts = Post::select(['id', 'title', 'slug', 'published_at'])->get();
Optimize Pagination
// Use cursor pagination for large datasets
$posts = Post::orderBy('id')->cursorPaginate(20);
// Or use simplePaginate when you don't need page numbers
$posts = Post::simplePaginate(20);
Chunk Large Operations
// Process large datasets in chunks
Post::chunk(1000, function ($posts) {
foreach ($posts as $post) {
// Process each post
$this->processPost($post);
}
});
Advanced Caching Strategies
1. Multi-Layer Caching Architecture
<?php
class PostService
{
public function getPopularPosts(int $limit = 10): Collection
{
return Cache::remember(
"popular_posts_{$limit}",
now()->addHours(6),
function () use ($limit) {
return Post::withCount('views')
->orderBy('views_count', 'desc')
->limit($limit)
->get();
}
);
}
public function getPostBySlug(string $slug): Post
{
// Cache individual posts for 24 hours
return Cache::remember(
"post_slug_{$slug}",
now()->addDay(),
fn() => Post::where('slug', $slug)
->with(['category', 'author', 'tags'])
->firstOrFail()
);
}
public function invalidatePostCache(Post $post): void
{
// Clear related caches when post is updated
Cache::forget("post_slug_{$post->slug}");
Cache::forget("post_id_{$post->id}");
Cache::tags(['posts', 'homepage'])->flush();
}
}
2. Redis Optimization
Configure Redis for optimal Laravel performance:
<?php
// config/database.php
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_')),
'serializer' => 'php', // Faster than JSON
'compression' => 'lz4', // Compress large values
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
'read_write_timeout' => 60,
'persistent_connections' => true,
],
// Separate database for sessions
'sessions' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_SESSION_DB', '1'),
],
// Separate database for cache
'cache' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '2'),
],
],
3. HTTP Caching with ETags
<?php
class PostController extends Controller
{
public function show(Post $post)
{
// Generate ETag based on post and its relationships
$etag = md5($post->updated_at . $post->category->updated_at);
if (request()->header('If-None-Match') === $etag) {
return response(null, 304); // Not Modified
}
return response()
->view('posts.show', compact('post'))
->header('ETag', $etag)
->header('Cache-Control', 'public, max-age=3600');
}
}
Code-Level Optimizations
1. Efficient Collection Operations
❌ Inefficient
$posts = Post::all();
// Multiple loops through the same collection
$published = $posts->filter(fn($p) => $p->published);
$categories = $posts->map(fn($p) => $p->category);
$titles = $posts->pluck('title');
✅ Efficient
$posts = Post::all();
// Single loop with reduce or transform
[$published, $categories, $titles] = $posts->reduce(
function ($carry, $post) {
if ($post->published) $carry[0][] = $post;
$carry[1][] = $post->category;
$carry[2][] = $post->title;
return $carry;
},
[[], [], []]
);
2. Lazy Loading and Generators
<?php
class ExportService
{
public function exportPosts(): Generator
{
// Use lazy collections for memory efficiency
return Post::lazy()->map(function ($post) {
return [
'title' => $post->title,
'author' => $post->author->name,
'published' => $post->published_at->format('Y-m-d'),
];
});
}
public function processLargeDataset(): void
{
// Process without loading everything into memory
Post::lazy()->each(function ($post) {
$this->processPost($post);
// Optional: Pause between batches to reduce load
if ($post->id % 1000 === 0) {
sleep(1);
}
});
}
}
Queue Optimization
Batch Processing for Heavy Operations
<?php
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
class NewsletterService
{
public function sendToAllSubscribers(Newsletter $newsletter): void
{
$subscribers = User::whereNotNull('email_verified_at')
->chunk(100)
->map(fn($users) => new SendNewsletterJob($newsletter, $users));
// Process in batches with progress tracking
Bus::batch($subscribers)
->then(function (Batch $batch) {
Log::info('Newsletter sent to all subscribers');
})
->catch(function (Batch $batch, Throwable $e) {
Log::error('Newsletter batch failed', ['error' => $e->getMessage()]);
})
->finally(function (Batch $batch) {
Cache::forget('newsletter_sending');
})
->dispatch();
}
}
Performance Monitoring
1. Laravel Telescope Configuration
<?php
// config/telescope.php
'watchers' => [
Watchers\QueryWatcher::class => [
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
'slow' => 100, // milliseconds
],
Watchers\RequestWatcher::class => [
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
],
Watchers\CacheWatcher::class => [
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
],
],
2. Custom Performance Middleware
<?php
class PerformanceMonitoringMiddleware
{
public function handle(Request $request, Closure $next): Response
{
$startTime = microtime(true);
$startMemory = memory_get_usage();
$response = $next($request);
$duration = microtime(true) - $startTime;
$memoryUsed = memory_get_usage() - $startMemory;
// Log slow requests
if ($duration > 2.0) {
Log::warning('Slow request detected', [
'url' => $request->fullUrl(),
'method' => $request->method(),
'duration' => round($duration, 3),
'memory' => $this->formatBytes($memoryUsed),
'queries' => DB::getQueryLog(),
]);
}
return $response->withHeaders([
'X-Response-Time' => round($duration * 1000, 2) . 'ms',
'X-Memory-Usage' => $this->formatBytes($memoryUsed),
]);
}
private function formatBytes(int $bytes): string
{
return round($bytes / 1024 / 1024, 2) . 'MB';
}
}
Production Configuration
Optimized php.ini Settings
[PHP]
; Production performance settings
memory_limit = 256M
max_execution_time = 30
max_input_vars = 3000
; OPcache (critical for performance)
opcache.enable = 1
opcache.memory_consumption = 256
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 0
opcache.validate_timestamps = 0
opcache.save_comments = 0
opcache.fast_shutdown = 1
opcache.enable_file_override = 1
; Realpath cache
realpath_cache_size = 4096K
realpath_cache_ttl = 600
; Session handling
session.gc_probability = 0 # Handle via Laravel schedule
Laravel Production Optimizations
# Essential production commands
php artisan config:cache # Cache configuration
php artisan route:cache # Cache routes
php artisan view:cache # Cache Blade templates
php artisan event:cache # Cache events and listeners
# Optimize Composer autoloader
composer install --optimize-autoloader --no-dev
# Clear unnecessary caches
php artisan optimize:clear
# Generate optimized files
php artisan optimize
Performance Testing & Benchmarking
🔍 Essential Performance Metrics
Response Time Targets:
- • Homepage: < 500ms
- • Category pages: < 800ms
- • Search results: < 1000ms
- • Admin pages: < 1500ms
Database Targets:
- • Query time: < 100ms
- • Queries per request: < 20
- • No N+1 queries
- • Index usage: > 95%
Load Testing with Artillery
# artillery-config.yml
config:
target: 'https://your-app.com'
phases:
- duration: 60
arrivalRate: 10 # 10 users per second
- duration: 120
arrivalRate: 50 # Ramp up to 50 users per second
- duration: 60
arrivalRate: 100 # Peak load
scenarios:
- name: 'Browse blog'
weight: 70
flow:
- get:
url: '/'
- get:
url: '/blog'
- get:
url: '/blog/{{ $randomString() }}'
- name: 'Search and filter'
weight: 30
flow:
- get:
url: '/search?q=laravel'
- get:
url: '/blog?category=development'
Common Performance Anti-Patterns
❌ Loading Entire Models in Loops
foreach ($userIds as $id) {
$user = User::find($id); // Separate query for each user
}
Fix: Use User::whereIn('id', $userIds)->get()
❌ Unnecessary Model Instantiation
$count = User::all()->count(); // Loads all users into memory
Fix: Use User::count()
for database-level counting
❌ Missing Database Indexes
Post::where('status', 'published')->where('category_id', $id)->get();
Fix: Add compound index on ['status', 'category_id']
Conclusion: The Performance Mindset
Performance optimization isn't a one-time task - it's a mindset that should permeate every aspect of your Laravel development. Start with the database, leverage caching strategically, and always measure the impact of your changes.
Remember: premature optimization is the root of all evil, but so is ignoring performance until it's too late. Build performance monitoring into your development process from day one, and your future self (and users) will thank you.
Performance Optimization Checklist
Database:
- ✓ Eliminate N+1 queries
- ✓ Add proper indexes
- ✓ Optimize slow queries
- ✓ Use pagination
Application:
- ✓ Implement caching layers
- ✓ Optimize collection operations
- ✓ Use queues for heavy tasks
- ✓ Monitor performance metrics
Struggling with application performance issues? Let's chat about optimizing your Laravel application for scale and speed. I've helped teams improve response times by 10x while reducing server costs.

Chris Page
Fractional CTO and Software Engineer with 25+ years of experience. I help startups scale from 0 to 7 figures using AI-assisted development and proven frameworks.