0

Backpressure — slowing down producers when consumers can't keep up

Advanced5 min read·eng-20-012
interviewperformance

Concept

Backpressure — a mechanism that slows down producers when consumers can't keep up. Without backpressure, a fast producer overwhelms a slow consumer, causing memory exhaustion, queue overflow, or system collapse.

The producer-consumer problem:

  • Producer: generates work (HTTP requests coming in, events being published, jobs being dispatched).
  • Consumer: processes work (queue workers, database writes, API calls).
  • If producers are faster than consumers, the queue grows unboundedly → runs out of memory → crashes.

Backpressure signals the producer to slow down:

  • HTTP rate limiting (429 Too Many Requests) — tells clients to slow down.
  • Queue size limits — reject new jobs when queue is too large.
  • TCP flow control — the OS-level implementation of backpressure (receiver's buffer fills → sender slows automatically).
  • stream_set_write_buffer() — PHP stream backpressure.

Backpressure in Laravel queues:

  • If queue worker can't keep up, the queue depth grows.
  • Laravel Horizon monitors queue depth and alerts when it gets too large.
  • Adding more workers = increasing consumer throughput.
  • If consumers can never catch up → backpressure: reject new jobs with 429, or shed load.

Streaming with backpressure: When streaming large responses, the client must read as fast as the server produces. PHP's ob_flush() + flush() — blocks if the client's TCP buffer is full, applying natural backpressure.

Load shedding: When backpressure isn't enough, drop excess requests intentionally (503 Service Unavailable) to protect the system. Better to reject some requests than to crash for all of them.

Code Example

php
<?php
// BACKPRESSURE via rate limiting (HTTP layer)
Route::middleware('throttle:100,1')->post('/api/events', [EventController::class, 'store']);
// If client exceeds 100 requests/minute → 429 Too Many Requests
// Client MUST slow down — can't produce faster than this rate

// BACKPRESSURE via queue size check
public function dispatchJob(array $data): JsonResponse
{
    $queueSize = Queue::size('default');
    $maxQueue  = config('queue.max_pending', 1000);

    if ($queueSize >= $maxQueue) {
        // Consumer (workers) can't keep up → reject new work
        return response()->json([
            'error'   => 'System is under heavy load, please retry later',
            'retry_after' => 30,
        ], 503); // 503 Service Unavailable
    }

    dispatch(new ProcessDataJob($data));
    return response()->json(['queued' => true]);
}

// STREAMING with natural backpressure
Route::get('/stream/large-export', function () {
    return response()->stream(function () {
        $query = Order::query()->cursor(); // lazy, one at a time

        foreach ($query as $order) {
            echo json_encode($order) . "\n";
            ob_flush(); // flush to nginx/client
            flush();    // PHP-level flush
            // If client reads slowly, flush() blocks → PHP waits → DB cursor pauses
            // This IS backpressure: producer (cursor) slows to match consumer (client)
        }
    }, 200, [
        'Content-Type'      => 'application/x-ndjson',
        'X-Accel-Buffering' => 'no', // disable nginx buffering for streaming
    ]);
});

// HORIZON — monitor and respond to queue backpressure
// config/horizon.php
'environments' => [
    'production' => [
        'supervisor-1' => [
            'connection' => 'redis',
            'queue'      => ['default'],
            'balance'    => 'auto',        // auto-balance workers
            'minProcesses' => 1,
            'maxProcesses' => 10,          // scale up to 10 workers if queue backs up
            'balanceMaxShift'   => 1,
            'balanceCooldown'   => 3,
        ],
    ],
],
// Horizon alert when queue has >50 pending jobs and workers can't keep up:
// HorizonServiceProvider.php → Horizon::routeSmsNotificationsTo(...)