Independent comparison · no paid rankings
Home / Blog / Laravel queues: process jobs without forgetting failures
Technical

Laravel queues: process jobs without forgetting failures

A Laravel worker that drains the queue but ignores failed jobs eventually drops orders, emails, and webhooks into a black hole. Here is how to structure drivers, retries, and dead letters.

5 min read Updated Jun 19, 2026

Friday evening, the shop shows "order confirmed." The confirmation email never left. Monday morning, support finds an empty jobs table on the app side — but hundreds of rows in failed_jobs, all tied to an SMTP timeout after the weekend release.

This is common: the web UI responds quickly because Laravel dispatched the job, but nobody watched consumption or failures. Queues feel invisible until a worker stops, a misconfigured driver falls back to sync, or aggressive retries amplify an external outage.

Driver, connection, and what Laravel actually runs

A Laravel job is a serialized message (class, payload, attempts) stored in a backend — Redis, database, SQS, Beanstalkd. The worker (queue:work or Horizon) pops it, instantiates the class, runs handle(), then acknowledges or marks failure.

DriverWhen to use itCommon limit
redisProduction, multiple workers, low latencyRedis must be persistent and monitored
databaseSmall volume, no brokerContention on the jobs table under load
sqsAWS cloud, regional decouplingCost and DLQ visibility to configure
syncLocal tests onlyInline execution — no resilience

On shared hosting, managed Redis or a small VPS dedicated to the broker keeps web load separate from job consumption. On a single VPS, document who restarts the worker after deploy — a supervisor that never reloads lets the queue grow with no visible HTTP error.

A site answering in 200 ms can be functionally broken if workers stopped yesterday.

Retries, backoff, and idempotent jobs

Laravel retries jobs that throw, according to $tries, $backoff, or $retryUntil. That helps when a third-party API is briefly down. It is dangerous for jobs that move money, create invoices, or call non-idempotent webhooks.

Practical rules:

  • Exponential backoff instead of ten attempts in ten seconds on the same structural error.
  • $maxExceptions to stop a broken job before it exhausts the queue.
  • Idempotence — a second run must not double the effect (unique DB key, Redis lock, "already processed" status).

ShouldBeUnique jobs and rate-limiting middleware also prevent storms when a hundred thousand orders trigger the same handler.

Failed jobs, Horizon, and alerts that matter

The failed_jobs table (or Horizon UI) is your queue of forgotten work. Many teams run the migration and never look again.

Set up:

  1. Alerts when failed_jobs > 0 on critical queues (payments, notifications).
  2. Horizon dashboard or Prometheus metrics on queue_size, jobs_processed, failed_jobs_total.
  3. Runbook — who retries (queue:retry all), who deletes after fix, who logs the incident.

Horizon centralizes Redis supervision: wait time, throughput, active workers. Without Horizon, a cron that checks the oldest job age in Redis already detects a dead worker.

Deploy, Supervisor, and the zombie worker trap

A deploy that replaces code without restarting workers may run old job code for hours. Laravel documents queue:restart for a graceful reload.

Deploy checklist:

  • php artisan queue:restart after code update.
  • Supervisor (or systemd) with autorestart=true.
  • Split heavy queues (--queue=default,emails) so urgent jobs are not blocked.

On Docker Compose or Kubernetes, a worker is not "just another container" — it needs the same image version and env vars as the app, plus a health check that verifies consumption, not just that the process exists.

The peak: the queue hides a business outage

Here is what five-minute Laravel queue tutorials skip.

The queue is a deferred promise. Without observability and a failed_jobs culture, you outsource the problem to customer support or a partner who never receives your callback.

Decide and move forward without blind spots

Map your jobs: which are critical, which tolerate delay, which must be idempotent. Pick a driver that fits your hosting (Redis on VPS, SQS in cloud). Configure backoff retries, failure alerts, and queue:restart in your deploy pipeline.

To compare offers with managed Redis, dedicated workers, or bundled monitoring, browse our directory and compare tool. Guides and by use case pages help size infra around your PHP stack.

Concrete test: kill a worker in staging, dispatch ten jobs, verify alerts and failed_jobs content. If nobody is notified within fifteen minutes, the queue is not production-ready.

Frequently asked questions

Which queue driver should I use with Laravel?

Redis or SQS for production with multiple workers; database for small volumes. Never leave sync in production — it hides concurrency and timeouts.

What should I do with jobs that fail after all retries?

They land in failed_jobs. Alert, analyze, fix, then retry with queue:retry or delete with traceability — do not let them accumulate silently.

Do I need Horizon on a small VPS?

Once you run multiple workers or critical jobs, Horizon simplifies supervision. With a single process, health cron and failed_jobs alerts may be enough initially.

How do I size the number of workers?

Split queues by workload type and measure your slowest job. Adding workers to a queue blocked on external calls only creates more parallel connections, not useful throughput.


Next time a deploy "succeeds," check one thing: is a worker still processing the queue — and do failures surface somewhere?

Compare European hosts

Filter by compliance, location and use case — then open the sheets to verify the real scope.

Browse the directory
Blog

Related reading

All articles →