test3
vb

Queues are the secret engine behind many apps. They can turn slow, fragile tasks into fast, reliable workflows. Whether you’re sending thousands of emails, processing videos, or orchestrating complex pipelines.
This article goes behind the curtains on how Laravel’s queue system works. From the moment you dispatch a job, to how it’s serialized, stored, popped, executed and retried. We’ll trace the full lifecycle of a message, looking into workers, visibility timeouts, backoff, chains and batches.
Get a coffee and prepare to really understand how queues work in Laravel (and in general).
Why Queues Matter
Queues keep your app fast and reliable. By moving slow or risky work off the request cycle, Laravel lets you return responses quickly, absorb traffic spikes, run tasks in parallel and even retry them in a safe way if something goes wrong. They decouple responsibilities (web -> workers), isolate failures, and give you control over timing, and priority.
Common wins
- Offload I/O-heavy tasks: emails, webhooks, PDFs, image/video processing, AI/API calls.
- Improve reliability: retries with backoff and timeouts.
- Scale predictably: add workers without changing business code.
Core Concepts
Before diving into how Queues work in Laravel, let's learn some concepts that we will be using in this article that can help make things simpler.
- Job: A small class (
ShouldQueue
) whosehandle()
does the work. - Dispatch: Sends a job to a connection with metadata (queue, delay, attempts).
- Connection: The backend you use (Redis, Database, SQS), chosen by name.
- Queue (name): A lane within a connection for priorities (high, default, low).
- Driver/Connector: The implementation that pushes/pops jobs on the connection.
- Payload & serialization: JSON envelope where models serialize to IDs and closures via
SerializableClosure
. - Worker:
php artisan queue:work
, a long-running process that pops, runs middleware, and acknowledges jobs. - Visibility timeout: The “lock” window. It must exceed job runtime to avoid duplicates.
- Acknowledgement: Delete on success. Release to retry later. Bury/fail after limits.
- Retries & backoff: Control how often and how long to wait between attempts.
- Failures: Failed jobs land in
failed_jobs
. - Middleware: Cross-cutting rules like throttling and rate limiting.
- Chains & batches: Sequential flows and grouped jobs with progress/failure rules.
- Idempotency: Safe to run twice. Use unique keys, upserts, and guards.
High-Level Architecture
If we look it in a really simplified way, Laravel's Queue System is a simple pipeline: create a job, serialize it, store it, pop it, run it, and acknowledge it. All of it, having safety mechanisms for retries and failures.
Dispatch
You call Job::dispatch(...)
. The Bus serializes the job (class + data + metadata) and hands it to the Queue Manager.
Connector
The Queue Manager picks a connector (Redis, Database, SQS) and pushes a JSON payload onto the chosen queue (name/lane).
Worker
The php artisan queue:work
runs as a long‑lived process. It reserves a job, locks it (visibility timeout), and resolves dependencies from the container.
Middlewares
Throttling, retries, rate limits, etc., wrap the Jobs handle()
method.
Acknowledge or retry
On success, deletes the job. Exceptions trigger release (retry with backoff) or mark as failed after maxTries
.
Example
class ProcessReport implements ShouldQueue
{
use Queueable, SerializesModels;
public int $timeout = 30;
public function __construct(public Report $report) {}
public function backoff(): array
{
return [5, 30, 90];
}
public function handle(): void
{
// Process report logic here
}
}
// Dispatch after DB commit, on a high-priority queue
ProcessReport::dispatch($report)
->afterCommit()
->onQueue('high');
# Running the worker
php artisan queue:work --queue=high,default --timeout=35 --max-jobs=1000 --max-time=3600
Dispatch Lifecycle
Before diving a little deeper on how Laravel handles the dispatch logic. Let's check a high-overview on how the job goes from your code to the queue connection.
- You dispatch the job:
Job::dispatch(...)
and Laravel's Bus collects the options (queue, connection, delay, etc). - The job is serialized (we're going to check how this works in the next section).
- The Queue Manager picks a connection (Redis, Database, SQS) and queue name, uses a connector to push the payload, respecting delay/visibility settings.
Now that we have an idea on how it works, let's dive a little more on what happens when you call Job::dispatch(...)
.
The Dispatchable
trait's dispatch()
method returns a PendingDispatch
wrapper around your job and chained calls like ->onConnection()
, ->delay()
, are proxied to the job.
The Queueable
trait is the one that provides these fluent methods above and where the properties like $connection
, $delay
, and others are set.
When the PendingDispatch
dispatches, either by explicitly calling the dispatch()
method or implicitly on destruction, the Bus checks:
- ShouldQueue: decides between dispatching to queue vs run inline. It also checks if it should dispatch only after the response is sent.
- Connection/queue:
$job->connection
and$job->queue
(or config defaults). - Timing:
$job->delay
,$job->timeout
,backoff()
,retryUntil()
- Safety:
ShouldBeUnique/uniqueId()
,ShouldBeEncrypted
,afterCommit
The Queue Manager then picks the connector (Redis/Database/SQS), applies delay/visibility settings, and pushes a JSON payload that includes the job class, data, and all the collected metadata.
Payload and Serialization
When you dispatch a job in Laravel, the framework wraps it in a compact JSON “envelope” that workers can pop, decode, and run safely and repeatedly.
What’s inside the envelope
Below we're going to see some of the most important values that are stored in the JSON "envelope" that Laravel creates when dispatching a job.
- job: Always
Illuminate\Queue\CallQueuedHandler@call
(the generic invoker). - uuid and displayName: For tracking and logs.
- data.commandName: Your job class name.
- data.command: A serialized clone of your job object.
- Timing properties:
timeout
,timeoutAt
,retryUntil
,backoff
,maxTries
,maxExceptions
- tags: For metrics.
- encrypted:
true
(only if the job implementsShouldBeEncrypted
).
How the serialization works under the hood
Below we're going to see how the serialization works for different types of data when dispatching a job.
- Job object: The Bus clones your job and serializes it using PHP’s
serialize()
. Drivers store that serialized blob inside JSON (and may base64 it). - Eloquent models: If your job uses the
SerializesModels
trait, each model becomes a lightweightModelIdentifier
(class, id, connection, relations). On the worker, Laravel re-hydrates models by re-querying the database—keeping payloads tiny and consistent. - Closures: Closure-based jobs are serialized using the
laravel/serializable-closure
package. They work, but prefer concrete job classes for safer rolling deploys. - Encryption: Jobs that implement the
ShouldBeEncrypted
interface have their serialized payload encrypted before being written to the connection.
JSON envelope example
{
"uuid": "b4c1e7c6-...",
"displayName": "App\\Jobs\\ProcessReport",
"job": "Illuminate\\Queue\\CallQueuedHandler@call",
"maxTries": 3,
"timeout": 30,
"backoff": [5, 30, 90],
"tags": ["important", "client:42"],
"data": {
"commandName": "App\\Jobs\\ProcessReport",
"command": "SERIALIZED JOB HERE"
}