Scaling Laravel Queues to 1M Jobs a Day
Last October, a marketing push sent our order volume from 35k to over a million queue jobs per day. Nothing broke — because the boring decisions we'd made months earlier quietly did their job. Here's the whole playbook.
1. Give every queue a budget
We split one default queue into seven named queues — orders, emails, webhooks, ai-jobs, exports, imports, maintenance — each with its own worker pool and concurrency ceiling. A stuck export can no longer starve checkout emails.
'environments' => [ 'production' => [ 'orders' => ['processes' => 12, 'tries' => 3], 'ai-jobs' => ['processes' => 4, 'timeout' => 90], ] ]2. Batching is a superpower — with guardrails
Laravel's Bus::batch() turned our 40k-row catalog re-embedding from a 6-hour crawl into a 22-minute job with progress tracking. The guardrail: batches get a concurrency allowance, not unlimited parallelism.
"Queues are a promise, not a dumpster. If you can't describe a job's failure mode in one sentence, it's not ready for production."
3. Dead letters are a feature
Every queue routes permanent failures to a failed_* mirror with the full payload. A daily job digests them, groups by exception class, and posts a digest to Slack.
The unsexy truth: scaling queues is mostly about boundaries — named queues, rate limits, idempotent handlers and a paper trail for failures.