Bottleneck — the single slowest part of a system that limits total throughput
Concept
Bottleneck — the part of a system that limits overall throughput or response time. Like the neck of a bottle restricting flow, the slowest component determines the system's maximum speed.
Theory of Constraints: In any system, there is always ONE bottleneck. Optimizing anything else doesn't improve overall performance. You must find and fix the bottleneck; then a new bottleneck emerges elsewhere.
Common bottlenecks in web applications (in order of frequency):
- Database queries: N+1 queries, missing indexes, slow unoptimized queries, table scans.
- External API calls: Slow third-party services. Solution: cache, async, timeouts.
- Rendering/computation: CPU-intensive code. Solution: cache the result, move to background job.
- I/O: Disk reads, network calls. Solution: caching, async processing.
- Memory: Swapping to disk (out of RAM). Solution: optimize data structures, more RAM.
- CPU: Rare in web apps. Usually I/O is the limit, not CPU.
Finding the bottleneck: Profile first. Don't guess. A profiler shows exactly where time is spent. The function with the highest total time is the bottleneck.
Fixing the bottleneck:
- DB query: Add index, fix N+1, rewrite query, add cache.
- External API: Add HTTP timeout, cache responses, move to queue.
- Slow code: Optimize algorithm, cache result, use background job.
- DB connection: Add connection pool (ProxySQL, PgBouncer).
After fixing: Rerun the profile. The bottleneck is gone. A NEW one will appear — that's the next thing to fix. Repeat until performance is acceptable.
Code Example
<?php
// FINDING bottlenecks with query logging
public function ordersPage(): View
{
DB::enableQueryLog();
$start = microtime(true);
// Code under investigation:
$orders = Order::where('status', 'pending')->paginate(20);
foreach ($orders as $order) {
$order->user; // ← N+1! Triggers query per order
$order->items; // ← N+1! Triggers query per order
}
$queries = DB::getQueryLog();
$duration = (microtime(true) - $start) * 1000;
// Prints: "Total time: 340ms, Queries: 41"
// 1 query for orders + 20 for users + 20 for items = 41 queries!
Log::debug("Total time: {$duration}ms, Queries: " . count($queries));
return view('orders.index', compact('orders'));
}
// FIX: eager loading eliminates the N+1 bottleneck
public function ordersPage(): View
{
$orders = Order::with('user', 'items') // 3 queries total, not 41
->where('status', 'pending')
->paginate(20);
// Now: "Total time: 12ms, Queries: 3" — 28x improvement!
return view('orders.index', compact('orders'));
}
// NEW BOTTLENECK after fixing N+1: the ORDER query is now the limit
// EXPLAIN SELECT * FROM orders WHERE status = 'pending' LIMIT 20
// → type: ALL (full table scan!), examined 50000 rows
// FIX: add index on status column
Schema::table('orders', function (Blueprint $table) {
$table->index('status'); // now type: ref, examines only 20 rows
// Or composite if frequently combined:
$table->index(['status', 'created_at']); // for sorted pending orders
});
// Result: "Total time: 3ms, Queries: 3" — further 4x improvement!
// BOTTLENECK WATERFALL — keep fixing until acceptable
// Before: 340ms (N+1)
// After fix 1: 12ms (missing index)
// After fix 2: 3ms (acceptable!)