Hangfire后台任务不执行的主因是服务注册、存储配置、工作线程三者缺一;Program.cs中必须按序配置AddHangfire(配存储)、AddHangfireServer(启工作线程)、UseHangfireDashboard(启仪表盘),缺任一环则任务卡在Scheduled或404。

Hangfire后台任务不会自动执行,哪怕你写了 BackgroundJob.Enqueue 或 RecurringJob.AddOrUpdate,只要服务注册、存储配置、工作线程三者缺一,任务就永远卡在“Scheduled”或直接不入队。
Program.cs 里必须配齐三件套
ASP.NET Core 6+ 项目中,AddHangfire、AddHangfireServer、UseHangfireDashboard 必须按顺序出现在 Program.cs 中,漏掉任一环就会报错或静默失败。
-
AddHangfire负责注册存储(比如 SQL Server),必须传入连接字符串和序列化配置;不配存储 → 任务不持久,重启即丢 -
AddHangfireServer必须紧跟其后,它启动后台工作线程;没这句 → 所有任务状态永远停在 Scheduled/Enqueued,不会真正执行 -
UseHangfireDashboard要放在app.UseEndpoints之前;放错位置 → 访问/hangfire直接 404 - 常见错误:
No service for type 'Hangfire.IBackgroundJobClient'就是AddHangfire没调用,或调用顺序被覆盖
延迟任务用 BackgroundJob.Schedule,别和 Enqueue 混用
BackgroundJob.Enqueue 是立即入队、尽快执行;BackgroundJob.Schedule 才是带延迟的,两者底层队列不同,参数语义也不同。
- 延迟 3 分钟发邮件:
BackgroundJob.Schedule(() => emailService.Send("user@ex.com"), TimeSpan.FromMinutes(3)) - 如果误写成
Enqueue+Task.Delay,那只是主线程等 3 分钟再入队,既阻塞又不可靠 - 时间精度:默认最小单位是秒;毫秒级延迟不支持,别传
TimeSpan.FromMilliseconds(500) - 注意闭包陷阱:不要在 lambda 里捕获循环变量,比如
for (int i = 0; i Console.WriteLine(i), ...),最后可能全输出 10
Cron 表达式时区不对,周期任务就跑偏
Hangfire 默认按服务器本地时区解析 Cron,不是 UTC。你在开发机上写 "0 0 * * *"(每天 0 点),部署到 UTC 时区的 Docker 容器里,实际就是北京时间早上 8 点执行。
- 解决办法一:显式指定时区,比如
RecurringJob.AddOrUpdate("daily-cleanup", () => Cleanup(), "0 0 * * *", TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")) - 解决办法二:统一容器时区,在 Dockerfile 加
ENV TZ=Asia/Shanghai && ln -snf /usr/share/zoneinfo/$TZ /etc/localtime - Cron 格式是「分 时 日 月 周」,不带秒;若需秒级(如
"*/30 * * * * *"),得换Hangfire.Cronos包并改用Cronos.Cron.Parse - 表达式写错最常见现象:任务状态长期卡在 “Scheduled”,Dashboard 里看不到下一次触发时间
并发任务重复执行?加 DisableConcurrentExecution 过滤器
同一个 recurring job 如果执行时间超过间隔(比如每 5 分钟跑一次,但某次清理数据库花了 7 分钟),默认会并发启动新实例,可能造成数据错乱。
- 加过滤器最简单:
RecurringJob.AddOrUpdate("sync-orders", () => orderService.Sync(), "0 */2 * * *", new RecurringJobOptions { QueueName = "critical", Deduplicate = true })—— 注意Deduplicate = true是 Hangfire 1.8+ 的写法 - 老版本用
[DisableConcurrentExecution(300)]特性标记任务方法,但要求该方法是 public static 或注入到 DI 容器里的实例方法 - 过滤器只对 recurring job 有效;
Enqueue和Schedule本身不触发并发控制,要靠业务层加锁或幂等设计 - 别依赖 Cron 表达式“避开高峰”,它不保证精确调度;高一致性场景建议用分布式锁(如 Redis Lock)兜底
最容易被忽略的是:RecurringJob 的 jobId 是字符串键,重复调用同名 jobId 会覆盖旧任务——上线新 Cron 表达式时,别忘了检查是否意外删掉了正在运行的定时逻辑。











