n8n Cron Trigger 使用 UTC 时区的坑:一次完整排查与解决记录

问题背景

我使用 n8n 做自动化,通过 Cron Trigger,每天早上 7 点自动执行一个工作流: - 抓取 当天天气预报 - 获取 农历日 - 组合成消息,推送到微信 / 企业微信

N8N 容器启动时已经明确设置: - TZ=Asia/Shanghai - GENERIC_TIMEZONE=Asia/Shanghai

运行多年都没问题。但最近升级了n8n之后,出现问题:获取到的是头一天的日期和农历。

原因排查

我第一反应是容器的时区没有指定为东八区,

Code node 中生成的日期慢了一天

工作流中有一个 Code node,用来生成当天日期,后续用这个日期去查农历:

let currentDateTime = new Date();
let year = currentDateTime.getFullYear();
let month = (currentDateTime.getMonth() + 1).toString().padStart(2, '0');
let day = currentDateTime.getDate().toString().padStart(2, '0');

return { currentDate: `${year}${month}${day}` };

1 月 18 日早上 7 点(中国时间) 执行时,得到的却是:

20260117

直接导致日期、农历错位。

二、排查过程

✅ 第一步:确认容器时区是否正确

docker exec -it n8n date

输出:

Sun Jan 18 22:48:54 CST 2026

结论:

✔ Docker 容器时区 完全正确


❌ 第二步:怀疑 TZ / localtime 是否未生效

尝试过:

结果:

Cron Trigger 仍然使用 UTC

说明这不是 Docker / Linux 层面的问题。


❌ 第三步:怀疑是 n8n Code node 的 bug

测试代码:

return { now: new Date().toISOString() };

输出:

2026-01-18T14:45:18.537Z

看起来像是 UTC,但这一步反而帮我确认了关键事实。


四、真相:这是 n8n 的“设计行为”,不是 Bug

⚠️ 核心结论

n8n 的 Cron Trigger 永远使用 UTC 进行调度

这是官方设计选择,原因包括:

同时要明确三点:

项目 是否受 TZ / GENERIC_TIMEZONE 影响
Docker 容器时间 ✅ 是
Code node Date() ❌ 默认 UTC
Cron Trigger ❌ 永远 UTC

也就是说:

Cron ≠ 系统时间 ≠ Code node 本地时间

这是大多数人第一次踩坑的根源。


五、正确的解决方案

✅ 方案一:Cron 时间手动减 8 小时(最推荐)

如果你希望:

每天 22:00(中国时间)执行

Cron 应该写成:

14:00 UTC

即:

0 14 * * *

常用对照表:

中国时间 Cron(UTC)
08:00 00:00
09:30 01:30
12:00 04:00
22:00 14:00

这是 生产环境最稳妥的做法


✅ 方案二:Code node 中显式指定时区(处理日期)

在 Code node 中,永远不要直接用:

getFullYear()
getMonth()
getDate()

正确写法:

const tz = 'Asia/Shanghai';
const now = new Date();

const parts = new Intl.DateTimeFormat('en-CA', {
  timeZone: tz,
  year: 'numeric',
  month: '2-digit',
  day: '2-digit',
}).formatToParts(now);

const map = Object.fromEntries(parts.map(p => [p.type, p.value]));

return {
  currentDate: `${map.year}${map.month}${map.day}`
};

输出:

20260118

✅ 方案三:Cron 高频触发 + 本地时间判断(进阶)

如果你不想心算 UTC:

示例:

const hour = Number(
  new Date().toLocaleString('en-US', {
    timeZone: 'Asia/Shanghai',
    hour: '2-digit',
    hour12: false,
  })
);

if (hour === 22) {
  return [{ run: true }];
}

return [];

适合复杂条件调度,但不适合简单定时。


六、强烈不建议的做法

❌ 依赖容器时区影响 Cron
❌ 使用 toISOString() 生成业务日期
❌ 在 Code node 中假设 Date() 是本地时间
❌ 试图“修复” n8n 的 Cron 行为

这些都会让问题在将来再次出现。


七、最佳实践总结(TL;DR)

如果你的自动化流程:

那么这个问题 迟早会踩

早点理解,少掉几个坑。


八、延伸

后续可以继续深入的点包括:

如果你也踩过这个坑,希望这篇记录能帮你节省几个小时的排查时间。