5 min read

Bus::bulk() in Laravel 13.13: Fast Dispatch Without Batch Overhead

Laravel 13.13 adds Bus::bulk() for high-volume job dispatch. How it groups jobs by queue and connection, and when to prefer it over batch() or dispatch().

Featured image for "Bus::bulk() in Laravel 13.13: Fast Dispatch Without Batch Overhead"

If you have ever needed to queue ten thousand jobs at once, you already know the two bad options Laravel offered. Loop over dispatch() and pay for one insert per job. Reach for Bus::batch() and get progress tracking you did not ask for, along with a database write on every single job completion to maintain it. Laravel 13.13, released June 2, 2026, adds a third option built specifically for this gap: Bus::bulk().

It is a small addition, a single static method, but it fixes a real and common annoyance. If you have ever written Queue::bulk() directly against the queue contract because Bus::batch() felt like too much machinery for a plain import job, this is the framework finally giving that pattern a proper home.

The problem it solves

Picture a command that needs to queue a job for every row in a large import, or fan out notifications to fifty thousand users after a price change. Three options existed before 13.13:

// Option 1: a loop. Simple, but one dispatch per job.
foreach ($users as $user) {
    ProcessUser::dispatch($user);
}

// Option 2: Bus::batch(). Gives you tracking you may not need,
// and writes to the batches table as each job finishes.
Bus::batch(
    $users->map(fn ($user) => new ProcessUser($user))
)->dispatch();

// Option 3: reach past the public API into Queue::bulk() directly.
Queue::bulk(
    $users->map(fn ($user) => new ProcessUser($user))->all(),
    '',
    'default'
);

Laravel developer Jack Bayliss described exactly this frustration in the pull request that introduced the feature, noting that dispatching in a loop is resource-intensive at scale, Bus::batch() writes to the database on every job completion just to track progress, and reaching for Queue::bulk() directly “felt a bit funny.” The PR’s original name was dispatchBulk(), but it shipped as the shorter Bus::bulk().

What Bus::bulk() actually does

Bus::bulk() takes an array or collection of job instances, groups them by their configured connection and queue, and pushes each group through the queue driver’s underlying bulk() method:

use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;

Bus::bulk(
    $users->map(fn ($user) => new ProcessUser($user))
);

That is the entire API surface. There is no batch ID, no then() or catch() callbacks, and no row written anywhere to represent “this run.” Individual jobs that fail still land in your failed_jobs table exactly as they would with plain dispatch(). What you are buying is cheaper enqueueing, not workflow coordination.

The efficiency gain depends heavily on your queue driver. On Redis, bulk dispatch cuts down the round trips needed to push a large batch of jobs. On the database driver, it can collapse thousands of individual inserts into a single bulk insert statement, which is where the win is most dramatic. On SQS, the benefit is smaller. Laravel’s SQS driver does not currently translate Bus::bulk() into a single SendMessageBatch call, so it still iterates under the hood, meaning the dispatch-time savings there are modest at best.

Chunking large fan-outs

For genuinely large jobs, dispatch inside chunkById() rather than materializing every job instance in memory first:

use App\Jobs\SyncProductToSearch;
use App\Models\Product;
use Illuminate\Support\Facades\Bus;

Product::query()
    ->where('is_active', true)
    ->select('id')
    ->chunkById(1000, function ($products) {
        $jobs = $products->map(
            fn ($product) => (new SyncProductToSearch($product->id))
                ->onQueue('search-sync')
        );

        Bus::bulk($jobs);
    });

Chunking bounds memory usage on the producing side and lets workers start draining the queue while later chunks are still being generated, rather than handing the queue one enormous burst.

When to reach for it, and when not to

Bus::bulk() is the right tool when jobs are independent of one another, there are a lot of them, and you do not need Laravel to answer questions like “has this whole run finished” or “which jobs from this batch failed.” Search indexing, cache warming, notification fan-out, and record syncs to a third-party API are typical fits, provided each job can succeed or fail on its own without the others knowing.

Reach for Bus::batch() instead when the jobs represent one logical operation that needs a first-class batch ID, progress tracking, completion callbacks, or cancellation. Those are exactly the guarantees Bus::bulk() deliberately skips in exchange for a faster enqueue path. And for modest job counts, a plain dispatch() loop is still fine. The added complexity of a driver-specific bulk path is not worth it until dispatch overhead is a measured bottleneck, not a guess.

One practical detail worth building in from day one: Bus::bulk() gives you no run-level identifier, so if you will ever need to answer “did yesterday’s sync finish cleanly,” stamp your own correlation ID onto each job’s constructor and log against it. Nothing about bulk dispatch tracks that for you, and it is much easier to add on day one than to retrofit after a debugging session.

$runId = (string) Illuminate\Support\Str::uuid();

$jobs = $invoices->map(
    fn ($invoice) => new PushInvoiceToCrm($invoice->id, $runId)
);

Bus::bulk($jobs);

The bigger picture

Bus::bulk() is a narrow, well-scoped addition, and that is exactly what makes it useful. It does not try to replace Bus::batch(), and it is honest about being a transport optimization rather than a workflow tool. If your Laravel app queues large fan-outs regularly and you have been either eating the cost of a dispatch() loop or bolting on batch tracking you never use, this is a one-line change worth trying. Just keep the jobs independent, keep the payloads small, and give each job an ID rather than a fully hydrated model, since that is what makes the whole pattern hold up once you are pushing tens of thousands of them at once.

Sources