Laravel队列任务超时不能靠set_time_limit(),必须用$timeout属性或dispatch()->timeout(),并确保retry_after≥timeout+10秒;pcntl信号在生产环境不可靠,应为外部调用单独设超时。

任务超时不是靠 set_time_limit() 控制的,它在 Laravel 队列里基本无效;真正起作用的是 $timeout 属性和 dispatch()->timeout(),且必须配合 retry_after 配置才可靠。
为什么 set_time_limit() 在队列里完全不生效
因为 php artisan queue:work 是常驻进程,每次取一个新任务前,PHP 的执行时间计数器都会被重置。你在 handle() 里写 set_time_limit(10),只对当前任务“这一小段脚本”起效,而 worker 进程本身不会因此退出或中断。
- 常见错误现象:
file_get_contents()或未设超时的curl_exec()卡住,set_time_limit()完全没反应 - Supervisor 下跑
queue:work --timeout=60生效,但类里写set_time_limit(10)没用 -
sync驱动下“看起来有效”,只是退化成同步调用,不能代表真实队列行为
$timeout 和 retry_after 必须协同设置
这两个参数分属不同层级:$timeout 是 Laravel 框架层对单次任务执行的硬性截断;retry_after 是消息中间件(如 Redis、database)对任务“租约”的有效期控制。若不匹配,任务会提前被重试甚至丢弃。
-
retry_after必须大于$timeout,建议至少多留 10 秒缓冲(例如$timeout = 60,则retry_after = 70) - 这个值要写在
config/queue.php对应驱动配置里,比如'redis' => [..., 'retry_after' => 70] - database 驱动还要求
failed_jobs表存在且含uuid字段(Laravel 8.5+ 强制) - 命令行启动时的
--timeout=60会覆盖任务类里的$timeout,但无法覆盖retry_after
需要中断内部阻塞操作?别碰 pcntl_signal
想在任务中主动中断某个慢操作(比如无超时的 HTTP 请求、sleep()),有人会尝试用 pcntl_alarm() + pcntl_signal()。但实际几乎不可靠:
- Laravel 的
queue:work不调用pcntl_signal_dispatch(),信号永远卡在内核队列里,回调根本不会执行 - 绝大多数生产环境(Supervisor、systemd、Docker 默认镜像、云平台)禁用或拦截
SIGALRM -
pcntl_*扩展在共享主机和多数容器中默认关闭,上线前很难验证是否可用 - 真正可控的做法是:为外部调用单独设超时——
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000)、file_get_contents(..., false, stream_context_create([...]))
长任务或高稳定性要求?优先用进程级 timeout 命令
与其在 PHP 层折腾信号,不如在启动层做硬性截断。Linux timeout 命令能精确杀死整个进程组,比任何 PHP 超时机制都干净可靠。
- Supervisor 配置示例:
command=timeout --signal=SIGTERM 300 php artisan queue:work --sleep=3 --tries=3 -
300是总耗时上限(秒),包含任务执行、重试等待、空闲休眠全部时间 - 必须用
--signal=SIGTERM,避免SIGKILL导致数据库事务未提交、Redis 锁未释放 - 搭配
--tries=3和合理backoff,让失败任务有重试机会,而不是一超时就永久丢失
真正容易被忽略的点是:retry_after 和 $timeout 的数值差一旦小于 10 秒,任务就可能在完成前被底层驱动判定为“过期”并强行重投——这种失败不是代码问题,而是配置冲突导致的静默逻辑错乱。


















