To send email through a queue in Laravel 13, set QUEUE_CONNECTION to a real driver such as database or redis, create a mailable, send it with Mail::to($user)->queue(new WelcomeMail($user)), and run php artisan queue:work. The web request returns immediately while the worker delivers the message.
What do you need before you start?
- Laravel 13 on PHP 8.3 or newer
- Working mail settings in
.env(a local tool such as Mailpit or Mailtrap is fine) - A queue backend. The database driver needs no extra software
How do you configure the queue?
# .env
QUEUE_CONNECTION=database
MAIL_MAILER=smtp
MAIL_HOST=127.0.0.1
MAIL_PORT=1025The database driver stores jobs in a jobs table. New Laravel apps ship with the migration, so migrating is enough.
php artisan migrateHow do you create the mailable?
php artisan make:mail WelcomeMail --markdown=mail.welcome<?php
namespace App\Mail;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class WelcomeMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(public User $user)
{
}
public function envelope(): Envelope
{
return new Envelope(subject: 'Welcome to our app');
}
public function content(): Content
{
return new Content(markdown: 'mail.welcome');
}
}SerializesModels stores only the model id in the job payload and reloads the model when the job runs.
How do you queue the email?
Call queue() instead of send().
use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;
public function store(Request $request)
{
$user = User::create($request->validated());
Mail::to($user)->queue(new WelcomeMail($user));
return redirect('/dashboard');
}To always queue a mailable, implement the ShouldQueue interface on it. Then even send() will queue it.
use Illuminate\Contracts\Queue\ShouldQueue;
class WelcomeMail extends Mailable implements ShouldQueue
{
// ...
}How do you delay or route the email?
Mail::to($user)->later(now()->addMinutes(10), new WelcomeMail($user));
$mail = (new WelcomeMail($user))->onConnection('redis')->onQueue('emails');
Mail::to($user)->queue($mail);How do you run the worker?
php artisan queue:workRestart workers after each deploy with php artisan queue:restart, because workers keep the old code in memory. In production, run the worker under a process manager such as Supervisor.
How do you handle failed emails?
Failed jobs are written to the failed_jobs table. Inspect and retry them from the command line.
php artisan queue:failed
php artisan queue:retry allHow do you verify it works?
- Submit the form that sends the mail and confirm the response is fast
- Check that a row appears in the
jobstable - Run
php artisan queue:workand confirm the message appears in your mail inbox tool - In tests, use
Mail::fake()andMail::assertQueued(WelcomeMail::class)
public function test_welcome_mail_is_queued(): void
{
Mail::fake();
$this->post('/register', [
'name' => 'Ana',
'email' => 'ana@example.com',
'password' => 'password123',
'password_confirmation' => 'password123',
]);
Mail::assertQueued(WelcomeMail::class);
}What are the common mistakes?
- Leaving
QUEUE_CONNECTION=sync, which sends mail inside the request - Forgetting to start the worker, so jobs pile up
- Not restarting workers after changing mail code or config
- Queueing a mailable that holds a non-serializable object
FAQ
Should I use queue() or ShouldQueue?
Use ShouldQueue if the mail should never block a request. Use queue() when you decide per call.
Does Mail::queue work with the sync driver?
Yes, but it runs immediately in the same request, so you get no speed benefit.
How many times is a failed email retried?
That depends on the worker --tries option or the job attempts setting. Set it explicitly for production.



