The Problem Nobody Talks About
Most Laravel queue tutorials stop at dispatch() and php artisan queue:work. In production, the real challenges are: what happens when consumers fall behind producers, how do you safely handle poison-pill jobs, and how do you get meaningful signals out of your workers without drowning in noise?
This article addresses all three.
Backpressure: Slowing the Producer
Backpressure means the consumer signals the producer to slow down when it cannot keep up. Laravel has no built-in backpressure primitive, but you can build one cheaply with Redis and a rate-aware dispatcher.
// app/Queue/BackpressureDispatcher.php
final class BackpressureDispatcher
{
private const THRESHOLD = 5_000;
public function __construct(
private readonly Redis $redis,
private readonly string $queue = 'default',
) {}
public function dispatch(ShouldQueue $job): void
{
$depth = (int) $this->redis->llen('queues:' . $this->queue);
if ($depth >= self::THRESHOLD) {
throw new QueueSaturatedException(
"Queue [{$this->queue}] depth {$depth} exceeds threshold."
);
}
dispatch($job)->onQueue($this->queue);
}
}
The caller catches QueueSaturatedException and either retries with exponential back-off or returns a 202 Accepted with a Retry-After header. This keeps your queue depth bounded without dropping work.
Dead-Letter Queues Without a Broker
Laravel's failed_jobs table is a dead-letter store, but it lacks routing. A better pattern is a dedicated dead-letter queue that jobs are explicitly moved to after exhausting retries, preserving the original payload and failure context.
// app/Jobs/Concerns/RoutesToDeadLetter.php
trait RoutesToDeadLetter
{
public function failed(Throwable $e): void
{
DeadLetterJob::dispatch(
originalJob: static::class,
payload: serialize($this),
reason: $e->getMessage(),
failedAt: now(),
)->onQueue('dead-letter');
}
}
// app/Jobs/DeadLetterJob.php
final class DeadLetterJob implements ShouldQueue
{
public int $tries = 1;
public function __construct(
public readonly string $originalJob,
public readonly string $payload,
public readonly string $reason,
public readonly Carbon $failedAt,
) {}
public function handle(): void
{
// Persist to dead_letter_events table for audit + replay UI
DeadLetterEvent::create([
'job' => $this->originalJob,
'payload' => $this->payload,
'reason' => $this->reason,
'failed_at' => $this->failedAt,
]);
}
}
A Filament resource over dead_letter_events gives ops teams a replay button without touching artisan queue:retry.
Structured Job Observability
Log lines are noise. Structured spans are signal. Use Laravel's Queue::before and Queue::after hooks to emit consistent telemetry.
// app/Providers/QueueServiceProvider.php
public function boot(): void
{
Queue::before(function (JobProcessing $event) {
$this->startTimer($event->job->getJobId());
});
Queue::after(function (JobProcessed $event) {
$elapsed = $this->stopTimer($event->job->getJobId());
Log::channel('structured')->info('job.processed', [
'job' => $event->job->resolveName(),
'queue' => $event->job->getQueue(),
'duration_ms' => $elapsed,
'attempts' => $event->job->attempts(),
'connection' => $event->connectionName,
]);
});
Queue::failing(function (JobFailed $event) {
Log::channel('structured')->error('job.failed', [
'job' => $event->job->resolveName(),
'error' => $event->exception->getMessage(),
'attempts' => $event->job->attempts(),
]);
});
}
Pipe the structured channel to a JSON log driver and ingest into your observability stack (Grafana Loki, Datadog, etc.). You now have p95 job latency per class without a paid APM agent.
Key Takeaways
- Backpressure is a producer responsibility — check queue depth before dispatching and surface
Retry-Afterto callers. - Dead-letter queues should be first-class entities with a replay UI, not a graveyard in
failed_jobs. - Structured telemetry from
Queue::before/afterhooks gives you latency histograms and failure rates per job class for free. - Keep
$trieslow (2–3) and$backoffexponential; long retry chains mask systemic failures. - A dedicated
dead-letterqueue worker with a single process prevents replay storms from starving production queues.