laravel队列任务失败时默认不发邮件,必须在任务类的failed()方法中手动实现通知逻辑;因其独立于http生命周期,全局异常处理器无效,且failed()仅在最终失败时触发,需注意重试配置与内部异常兜底。

队列任务失败时,Laravel 默认不会发邮件——必须手动在 failed() 方法里写通知逻辑。
为什么全局异常处理器(App\Exceptions\Handler)对队列无效
因为队列进程(如 php artisan queue:work)是独立于 HTTP 请求生命周期运行的。哪怕你在 Handler::report() 里写了邮件发送,它也完全收不到队列中抛出的异常。
- 队列任务失败后,Laravel 直接走自己的失败处理流程,跳过所有请求级异常通道
-
try-catch包裹handle()里的代码也没用——捕获了不 re-throw,任务就“假装成功”了 - 唯一可靠入口是任务类自身的
failed(Throwable $exception)方法
怎么在 failed() 里安全发告警邮件
这不是简单调用 Mail::to()->send() 就完事。要兼顾可读性、可维护性和失败兜底。
- 先确保已运行
php artisan queue:failed-table && php artisan migrate,否则连失败记录都存不住 - 别在
failed()里依赖request()、session()或任何 HTTP 上下文对象——它们不存在 - 推荐按异常类型分路径处理,比如网络超时发给运维,数据校验失败发给开发:
if ($exception instanceof \GuzzleHttp\Exception\ConnectException) { Mail::to('ops@example.com')->send(new PaymentConnectionFailedMail($exception)); } - 记得加日志:
\Log::error()至少记下$exception->getMessage()和$exception->getTraceAsString()
重试次数和失败时机,直接决定 failed() 会不会被调用
failed() 不是每次失败都触发,只在“最终失败”那一刻执行一次——也就是达到最大重试次数之后。
- 默认只试 1 次,意味着第一次失败就进
failed();生产环境务必显式设protected $tries = 3; - 设
protected $backoff = [3, 6];可避免雪崩式重试,但要注意:总耗时 = 各次间隔之和 + 执行时间 - 如果想首次失败就告警(比如关键支付任务),可在
handle()中手动调用$this->fail($exception),但会跳过后续重试,慎用 -
retryUntil()返回一个时间点,比固定次数更灵活,适合有明确截止时间的任务
最容易被忽略的是:failed() 方法本身如果再抛异常,整个失败处理就静默吞掉了——既没邮件也没日志。务必在它内部做最简健壮封装,或者至少包一层 try-catch 记个错。











