php 152 lines · 3 tabs

Chunk-Process a Large Newsletter Send with Laravel Job Batching and Progress Tracking

Shared by codesnips Sep 2026
3 tabs
<?php

namespace App\Http\Controllers;

use App\Jobs\SendNewsletterChunk;
use App\Models\NewsletterSend;
use App\Models\Subscriber;
use Illuminate\Support\Facades\Bus;
use Illuminate\Http\Request;
use Throwable;

class DispatchNewsletterController extends Controller
{
    public function store(Request $request)
    {
        $data = $request->validate([
            'subject' => ['required', 'string', 'max:200'],
            'body'    => ['required', 'string'],
        ]);

        $send = NewsletterSend::create($data + ['status' => 'queued']);

        $jobs = Subscriber::subscribed()
            ->pluck('id')
            ->chunk(500)
            ->map(fn ($ids) => new SendNewsletterChunk($send->id, $ids->all()))
            ->all();

        $batch = Bus::batch($jobs)
            ->name("newsletter:{$send->id}")
            ->allowFailures()
            ->then(fn () => $send->markCompleted())
            ->catch(fn ($b, Throwable $e) => report($e))
            ->finally(fn () => $send->refresh()->settle())
            ->onQueue('mail')
            ->dispatch();

        $send->update(['batch_id' => $batch->id, 'status' => 'processing']);

        return response()->json([
            'id'       => $send->id,
            'batch_id' => $batch->id,
            'progress' => $send->progress(),
        ], 202);
    }
}
3 files · php Explain with highlit

Sending a newsletter to hundreds of thousands of subscribers in a single request is a recipe for timeouts and lost work. This snippet shows how to fan the send out into small, resumable chunks using Laravel's job batching, so a failure in one chunk never re-sends the whole list, and progress can be reported back to an admin UI in real time.

The DispatchNewsletterController is the entry point. Rather than looping over subscribers inline, it builds a batch of SendNewsletterChunk jobs — one per page of recipient IDs — using Subscriber::subscribed()->pluck('id')->chunk(500). Chunking by primary key (not by loading full models) keeps memory flat regardless of list size. The batch is created with Bus::batch(...), given ->allowFailures() so a single bad chunk doesn't cancel the run, and a ->then, ->catch, and ->finally callback set to mark the owning NewsletterSend record complete. The controller persists batch_id on the send so the frontend can poll it.

SendNewsletterChunk is the unit of work. It carries only the NewsletterSend id and an array of subscriber ids — small, serializable payloads that survive queue restarts. The InteractsWithBatch trait exposes $this->batch(); the guard if ($this->batch()->cancelled()) return; lets an admin abort a large send cleanly. Each recipient is wrapped in a try/catch so one malformed address increments a failure counter rather than throwing the whole chunk into the retry loop. Delivery counts are pushed back with an atomic increment, avoiding lost updates when many workers report concurrently.

The NewsletterSend model ties it together. progress() derives a percentage from Bus::findBatch($this->batch_id), reading the framework's own processedJobs() and totalJobs() so the number always matches reality even after retries. isFinished() and the sent_count / failed_count columns give the admin a durable audit trail after the batch record is pruned.

The key trade-off is granularity: a chunk size of 500 balances per-job overhead against blast radius on failure. Larger chunks mean fewer jobs but coarser retries; smaller chunks mean more queue traffic. Batching also requires a persistent batch store (the job_batches table), which is what makes progress queryable long after the individual jobs finish.


Related snips

Share this code

Here's the card — post it anywhere.

Chunk-Process a Large Newsletter Send with Laravel Job Batching and Progress Tracking — share card
Link copied