0

Bottleneck — the single slowest part of a system that limits total throughput

Beginner5 min read·eng-20-009
interviewperformance

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):

  1. Database queries: N+1 queries, missing indexes, slow unoptimized queries, table scans.
  2. External API calls: Slow third-party services. Solution: cache, async, timeouts.
  3. Rendering/computation: CPU-intensive code. Solution: cache the result, move to background job.
  4. I/O: Disk reads, network calls. Solution: caching, async processing.
  5. Memory: Swapping to disk (out of RAM). Solution: optimize data structures, more RAM.
  6. 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
<?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!)