Code review · PHP
Review a Laravel Queue Job
Medium Code review
Marketing wants a monthly newsletter. This Laravel job is dispatched once a month and runs on the default queue with --tries=3. There are 400,000 users.
Review it: list the problems, what would happen when it runs, and how to fix each one.
The code to review
There are 5 issues to find.
<?php
namespace App\Jobs;
use App\Mail\Newsletter;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Mail;
class SendNewsletter implements ShouldQueue
{
use Queueable;
public function handle(): void
{
$users = User::all();
foreach ($users as $user) {
Mail::to($user->email)->send(new Newsletter($user));
}
}
}
Write your review
Loading the editor… (JavaScript is required to run code in your browser.)
The issues and how to fix them
Show the issues
1. Retries resend to everyone
Critical, line 17-20
If the job fails half-way (a timeout, an SMTP error), the queue retries it from the start: the first 200,000 users get the newsletter again, up to three times.
Fix: Fan out: one small job per user (or per chunk) so a retry only repeats that unit, and record who was sent the issue (a newsletter_deliveries row with a unique key) so a repeat is skipped.
2. Loads every user into memory
Major, line 17
User::all() hydrates 400,000 Eloquent models at once; the worker runs out of memory.
Fix: Iterate with User::query()->select('id', 'email')->chunkById(1000, ...) or lazyById().
3. One job runs far longer than the worker timeout
Major, line 15-21
Sending 400,000 emails synchronously takes hours. Laravel's default job timeout (60 s) and retry_after will kill the job and hand it to another worker, which starts again from the beginning while the first may still be running.
Fix: Dispatch per-chunk jobs (Bus::batch) that each finish in seconds, and set timeout and retry_after consistently.
4. Ignores consent and unsubscribes
Major, line 17
Every user is emailed, including those who unsubscribed or never opted in. That breaks anti-spam law (CAN-SPAM, GDPR/PECR) and damages the sender reputation of all email from the domain.
Fix: Filter on a consent flag, honour unsubscribes, and include a one-click unsubscribe link (List-Unsubscribe header).
5. Sends synchronously instead of using the mail queue and rate limits
Minor, line 20
send() inside a job is fine for one email, but bulk sending without rate limiting will hit the email provider's limits and fail repeatedly.
Fix: Queue each mailable (Mail::to(...)->queue(...)) on a dedicated mail queue and use RateLimited job middleware.
Queue job reviews are about failure modes: what happens on retry, on timeout and at full data size. The phrase interviewers want is "jobs must be idempotent", followed by how you would make this one so.
Fixed version
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Support\Facades\Bus;
class SendNewsletter implements ShouldQueue
{
use Queueable;
public function __construct(public string $issue)
{
}
public function handle(): void
{
$jobs = [];
User::query()
->where('newsletter_opt_in', true)
->select('id')
->chunkById(500, function ($users) use (&$jobs) {
$jobs[] = new SendNewsletterChunk($this->issue, $users->pluck('id')->all());
});
Bus::batch($jobs)->onQueue('mail')->name("newsletter {$this->issue}")->dispatch();
}
}
// SendNewsletterChunk::handle() skips users already recorded in newsletter_deliveries
// (unique on issue + user_id), queues each mailable, and records the delivery.
Follow-up questions
- What is the difference between
timeoutandretry_afterin Laravel queues? - How would you monitor that the batch finished? (
Bus::batchcallbacks, Horizon)
More Code Clinic: Code Review problems
- Review a Laravel Orders Controller · Medium
- Review an Express User Search Endpoint · Easy
- Review a Payment Webhook Handler · Medium
- Review a Sign-Up Function · Medium
- Review a CSV Export Endpoint · Medium
- Review a React Search Component · Medium