Currently Available: Need a skilled Software Developer for your next project?
Categories
Laravel

Handling Failures in Queued Laravel Mail

In a Laravel application, queued mail lets a web request hand an email task to a background worker instead of waiting for the mail service. Laravel stores the task on a queue; a worker later retrieves it and tries to send the message. The web request only adds the task to the queue. A worker handles it later, and neither step confirms that the recipient received the email.

When a worker encounters an error, Laravel’s retry settings determine whether it tries the job again. Developers must set retry limits and run workers on the correct queue. They also need a plan for handling duplicate emails and failures that require manual recovery.

A queued mail still needs a worker

Calling Mail::queue(...) places a mail job on a queue. A mailable that implements ShouldQueue is queued even when code calls send. Laravel’s mail documentation describes this background sending behavior.

Configure the worker to use the same queue connection and queue name that stored the job. For example, if code places mail on a queue named emails, a worker watching only the default queue will leave those messages waiting. In production, Laravel recommends keeping workers alive with a process monitor such as Supervisor. When messages remain unsent, check which queues the worker watches and how many jobs are waiting in the queue.

Workers also affect mail during deployments and maintenance. Laravel does not process queued jobs during maintenance mode unless a worker runs with --force. Workers keep running between jobs, so restart them gracefully after deployment to load changed code. Laravel’s queue:restart command asks them to exit; a process monitor should start them again. To spot growing backlogs, use Laravel’s queue:monitor command and handle the QueueBusy event when a queue exceeds its configured threshold, as described in the queue documentation.

Configure retries and backoff

Laravel’s queue documentation sets the default attempt limit to one. A failed job therefore does not necessarily get another execution just because Laravel releases it after an exception. Configure an attempt limit with the worker’s --tries option or a supported job-level setting, and check the behavior against the Laravel version in use. The queue documentation says a job’s own attempt setting takes precedence over the worker’s setting; it does not establish that every job-level option applies directly to every queued mailable.

Laravel can count an attempt even when the job does not finish running the mail-sending code. Laravel counts events such as exceptions, manual or middleware releases, and timeouts toward the limit. When choosing an attempt limit, account for those events so a job does not exhaust its attempts sooner than expected.

Backoff sets how long Laravel waits before trying a released job again. The worker’s --backoff option or a supported job-level setting can control the delay. Without a backoff, Laravel releases an exception-failing job immediately. Its queue documentation gives an example schedule of 1, 5, and 10 seconds, with later delays remaining at 10 seconds. Choose a schedule that gives a temporary mail-provider problem time to clear without leaving messages delayed unnecessarily. Laravel also documents time-bounded retries with retryUntil and exception limits with maxExceptions for queue jobs.

When a queued email fails, a mailable’s failed method receives the failure as a Throwable if the method is defined. Use it to log the error or record that the email failed for the operation it supports. Include enough context to identify the affected message or operation. After a job uses all its allowed attempts, Laravel records it in failed_jobs when failed-job storage is configured. Operators can inspect the failures with queue:failed and retry a job by ID with queue:retry. A manual retry recovers a job after failure. It is separate from automatic attempts, which happen while the job remains eligible.

Prevent duplicate sends and check what success means

The worker timeout limits how long a worker runs a job. A queue connection’s retry_after setting controls when the queue makes that job available again. Laravel recommends setting the worker timeout several seconds shorter than retry_after; otherwise, the queue can make a job available again while the first worker is still processing it. The SQS driver uses the AWS-managed visibility timeout for this timing rather than Laravel’s retry_after. Laravel also notes that job timeouts require PHP’s PCNTL extension, and that I/O operations need their own timeouts when they do not respect the worker timeout.

Sending mail affects a service outside Laravel. A mail provider can accept a message, then the worker can fail before Laravel records the job as complete. If a retry runs, it can send the message again. Retries help recover from errors, but they do not guarantee exactly-once delivery. Decide whether each type of email can be sent twice, and make the code that handles it account for that risk.

Laravel’s MessageSent event means the mail transport sent the message. It does not confirm that the message reached the recipient’s inbox. The mail transport, which passes mail to an SMTP server or provider, treats sending as successful when that server or provider accepts the message for further delivery, as the Symfony Mailer documentation explains. Use provider-side delivery and bounce information when the application needs to track later outcomes.

Queued mail also needs access to the data it uses. If code queues a message inside a database transaction, a worker can pick up the job before that transaction commits and find that the expected records are not available. Laravel provides afterCommit to add the job to the queue only after the transaction commits. Use it when the email depends on changes made in that transaction, as described in the Laravel mail documentation.

What I'm building

Delegate tasks. Get software.

Give Vroni a GitHub issue, bug report, spec, or rough idea. It reads the repo, plans the change, writes code, runs checks, and works toward a review-ready pull request.

Take a look at vroni.com

Email updates

Usually a new article and a few links I found interesting.

No spam. Unsubscribe with one click.

Leave a Reply

Your email address will not be published. Required fields are marked *