Updated June 2026. Tested on Laravel 13 and PHP 8.4. Part of the Techalyst queue series. For how workers pull these jobs back out, see Laravel Queue Workers: How They Work.

When you dispatch a job, it does not run there and then. It gets turned into a small record and stored, in MySQL, Redis, or SQS, until a worker is ready. Knowing exactly what gets stored, and how, clears up a whole class of "why did my job have stale data" bugs.

What gets stored for each job

Every queued job is saved with a handful of attributes that let a worker run it later in the right order.

  • The queue it belongs to.
  • The number of attempts so far, starting at zero and incremented each run.
  • The reserved time, when a worker picked it up.
  • The available time, when it becomes eligible to run.
  • The created time.
  • The payload, the serialized job itself.

Why a separate queue at all

By default everything goes on one queue. But you will often want to split work, say put mail on a mail queue with its own dedicated worker, so a flood of report jobs never delays your password reset emails. The queue name is just a label that lets you point specific workers at specific kinds of work.

SendInvoice::dispatch($order)->onQueue('mail');

Attempts and availability

The attempts counter is how Laravel knows when to give up. Without a limit, a failing job would be retried forever; set --tries on the worker or $tries on the job and once attempts reach it, the job is marked failed and left alone.

The available time is normally now, so workers can pick the job up immediately. To hold a job back, dispatch it with a delay and Laravel records a future availability time.

SendInvoice::dispatch($order)->delay(now()->addMinutes(10));

Under the hood a delay just becomes a timestamp: pass a number of seconds and Laravel adds them to now, or pass a DateTimeInterface and it uses that exact moment. Until that time passes, no worker will touch the job.

The payload, and why jobs are serialized

To store a job and rebuild it later in a different process, Laravel serializes it. Serializing a PHP object produces a string capturing its class and its current state, which can be turned back into a live object when a worker is ready. The stored payload records the class name and a serialized copy of the job, plus useful metadata like its display name, max tries, and timeout, which it reads straight off your job's properties.

class SendInvoice implements ShouldQueue
{
    public $tries = 3;
    public $timeout = 60;
    // these end up in the payload automatically
}

Why a clone is serialized, not the job itself

Here is a subtle but important detail. Laravel serializes a clone of your job, not the original instance. The reason is that serialization can mutate properties through PHP's __sleep magic method, and you do not want those mutations leaking back into the object you are still holding in the dispatching code.

Picture a job that carries an object holding a file handle. To serialize safely, that object's __sleep swaps the live file resource for its path, and __wakeup reopens it on the other side.

public function __sleep()
{
    $this->pdf = stream_get_meta_data($this->pdf)['uri']; // resource -> path
    return ['customer', 'pdf'];
}

public function __wakeup()
{
    $this->pdf = fopen($this->pdf, 'a'); // path -> resource
}

If Laravel serialized your original job, that path-for-resource swap would happen on the instance still sitting in your controller, and the rest of your request would suddenly see a broken object. Cloning first means the transformation happens on a throwaway copy, and your original stays intact.

SerializesModels: keep payloads small and fresh

There is one more piece that solves the "stale data" problem outright. If your job stores an Eloquent model and you serialized the whole model, two things go wrong: the payload gets large, and by the time the worker runs, the model's data may be out of date.

Laravel's SerializesModels trait fixes both. Instead of serializing the entire model, it stores just the model's class and id, and re-fetches a fresh copy from the database when the job runs. Jobs created with make:job use this trait by default.

use Illuminate\Queue\SerializesModels;

class SendInvoice implements ShouldQueue
{
    use SerializesModels;

    public function __construct(public Order $order) {}
    // only the order id is stored; a fresh Order is loaded at run time
}

This is why a queued job should accept models and let the trait do its work, rather than passing raw arrays of already-loaded data. The job stays small in storage and always runs against current data.

Wrapping up

Dispatching a job stores a compact record: which queue, how many attempts, when it becomes available, and a serialized payload. Laravel serializes a clone so serialization side effects never touch your live object, and SerializesModels stores only model ids so payloads stay small and data stays fresh. Once you see the job as a stored, serialized record, queues stop feeling like magic.

Next: the queue configuration keys explained, or revisit how workers run these jobs. Questions welcome in the comments.