>Dudenkoff_
← Lab / Series · 9 parts

The mail room

Laravel queues on a live rig

Jobs, workers, failures, batches, retries. Every claim read straight from the tables and the source.


01
July 3, 2026

The minimal job

What a job actually is - built from the empty stub up: dispatch it, catch the empty-queue surprise, open the payload with a SELECT, hire the worker, and measure what the mailbox buys. The chapter the queue docs never wrote.

02
July 4, 2026

The model that never rode the queue

Pass an Eloquent model to a job and the payload keeps a claim tag - class, id, relation names, connection. The worker trades it for fresh rows: ships 999 when you dispatched 250, throws when the row is gone. Snapshots, DeleteWhenMissingModels, WithoutRelations - every corner verified live.

03
July 5, 2026

Beyond the minimal job

Five problems from real development, five jobs built for them: a reminder that waits, an order that respects its transaction, an invoice that refuses to exist twice, a card number nobody else can read, a checkout with a fast lane - every switch verified with a SELECT.

04
July 6, 2026

Where jobs go when they die

A job throws - one attempt, then the morgue: failed_jobs keeps the uuid, the cause of death and the payload intact. queue:retry raises the same job into today's code, #[Tries] and #[Backoff] buy it patience, and failed() speaks the last word. Every step read straight from the tables.

05
July 7, 2026

Queue the listener

Flip ShouldQueue on a listener and the spinner drops from 2004 ms to 5.3 ms. Then open the payload: CallQueuedListener carries the listener by name, the event by value, the model as a claim tag - and fire() → call() → handle() finishes the route. A queued listener is a job the framework wrote for you.

06
July 8, 2026

The limits of patience

A job can hang, throw, or give up on its own terms, and Laravel keeps a separate limit for each. #[Timeout] lives on the daemon worker, retryUntil() is a deadline not a count, release() ends in DONE and not the morgue, #[MaxExceptions] and $this->fail() cut it short. Six limits, read straight from the tables.

07
July 9, 2026

The door before the job

Two syncs for the same product, two workers, two DONE - and a job still in the queue. Job middleware wraps handle() in a pipeline: WithoutOverlapping is a lock, RateLimited a budget, ThrottlesExceptions a breaker, and each can end a job without ever running it. Read straight from cache_locks and failed_jobs.

08
July 10, 2026

What a group costs

Chain three jobs and the queue holds one row - the other two are serialized inside the first, the catch() closure riding with them. A chain carries the group in its payload; a batch keeps it in a job_batches row every worker locks with SELECT ... FOR UPDATE. Two ways to remember a group, two currencies. Read from the payload and the ledger.

09
July 11, 2026

Workers in production

Two workers on one table never collide: the reserve query ends in FOR UPDATE SKIP LOCKED, read straight from the Postgres log. Then the fleet: a reservation is a lease whose term lives in the reader, a lease cannot tell dead from slow (one job, two live pids), the timeout alarm kills before the lease frees, and Supervisor refills the slot. Four clocks, byte-exact.