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(...)