n8n 计划触发器时区设置与错误时间排查
n8n 计划触发器可能按纽约时间运行,而不是 UTC。了解 TZ、GENERIC_TIMEZONE 和工作流时区的优先级,修复夏令时导致的 1 至数小时偏差。
为什么 n8n 的计划触发器会在错误的时间触发
n8n 的计划触发器会在错误的时间触发,是因为 n8n 会从三个不同位置读取时区,修正其中一个位置只能解决部分问题。这三个位置分别是容器自身的 TZ 变量、实例默认值 GENERIC_TIMEZONE,以及单个工作流内部设置的时区。一次性设置这三项后,之后创建的每个计划都会在预期时间执行。
先纠正一个常见判断。全新部署的自托管 n8n 不会按 UTC(协调世界时)创建计划。容器时钟使用 UTC,因为官方镜像没有设置 TZ。计划调度是独立的一层;截至 August 2026,n8n 文档中 GENERIC_TIMEZONE 的默认值为 America/New_York,因此未修改的实例会按纽约时间触发 Schedule Triggers。这就是为什么用户报告的时差通常与其所在位置和 UTC 的实际时差不一致。在柏林的管理员设置 06:00 后,本地时间会在 12:00 触发;而在 3 月美国已切换到夏令时、欧洲尚未切换的几周内,则会在 11:00 触发。
三个时区层级,以及优先使用哪一个
TZ 是容器内的操作系统时区。n8n 文档将其描述为用于设置系统时区的变量,以控制脚本和 date 等命令返回的结果。它决定容器内的 date 输出内容、容器日志行中的时间戳、Code 节点中的 new Date() 返回值,以及您在容器内运行的任何 shell 脚本所看到的时间。它不会影响 Schedule Trigger 的触发时间。
GENERIC_TIMEZONE 是 n8n 实例时区。文档称其为 n8n 实例时区,并指出它对 Cron 等计划节点很重要。这里的 Cron 指标准的基于时间的计划语法,n8n 在 Schedule Trigger 中将其作为 Custom (Cron) 选项提供。
工作流时区按工作流单独设置。在画布中打开工作流,选择右上角的三个点,选择 Settings,然后修改 Timezone 值。它会覆盖该工作流中的 GENERIC_TIMEZONE。
对于 Schedule Trigger,优先级顺序是固定的。如果工作流设置了时区,n8n 使用工作流时区;否则使用 GENERIC_TIMEZONE 中的实例时区;如果实例时区也未设置,则使用内置默认值 America/New_York。在这个判断过程中不会检查 TZ。
对于节点内部的日期,答案取决于代码使用哪个时钟。Luxon 是 n8n 表达式背后的日期库,它使用 n8n 时区,因此 $now 和 $today 遵循与触发器相同的“工作流优先,否则实例”顺序。Code 节点中的原生 JavaScript new Date() 查询操作系统,因此遵循 TZ。这正是大多数混淆的来源:触发器的时间可能正确,但工作流写入的每个时间戳却相差数小时。
在 Compose 文件中同时设置三项
在文件中将 TZ 和 GENERIC_TIMEZONE 放在一起,避免设置其中一项后忘记另一项。下面的片段是一个可正常运行的服务中与时区相关的部分。文件的其余部分,包括反向代理和证书,来自在 VPS 上通过 HTTPS 自托管 n8n。
services:
n8n:
image: docker.n8n.io/n8nio/n8n
restart: unless-stopped
ports:
- "127.0.0.1:5678:5678"
environment:
- GENERIC_TIMEZONE=Europe/Berlin
- TZ=Europe/Berlin
- N8N_RUNNERS_ENABLED=true
- N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=true
volumes:
- n8n_data:/home/node/.n8n
volumes:
n8n_data:使用 docker compose up -d 应用更改,不要使用 docker compose restart。重启只会使用创建容器时的环境再次启动同一个容器,因此文件已发生变化,但运行中的进程不会变化。up -d 会检测到环境已更改,并重新创建容器。如果将这些值内联改为放在 env 文件中,同样适用重新创建规则;Compose env 文件和机密处理指南介绍了该文件的读取位置。
在 Region/City 中使用 IANA(Internet Assigned Numbers Authority)时区名称,例如 Europe/Berlin 或 America/Sao_Paulo。这些名称包含对应地区的夏令时规则,因此当地时钟调整时,偏移量也会变化。Etc/GMT+5 这类固定偏移名称不会随季节变化,而且其符号方向与直觉相反。运行 LC_ALL=C TZ=Etc/GMT+5 date +%z,它会输出 -0500。请避免使用这些名称。
仅设置其中一个为什么只能修复一半
单独设置 GENERIC_TIMEZONE 后,Schedule Trigger 会在您指定的小时触发,但所有读取操作系统时间的组件仍使用 UTC。调用 new Date().toString() 的 Code 节点会返回 UTC 字符串,容器日志行会记录为 UTC,使用系统时钟生成的文件名也会在错误的午夜发生切换。
单独设置 TZ 后,情况正好相反。docker compose exec n8n date 会输出本地时间,看起来像是配置成功了,但 Schedule Trigger 仍使用 America/New_York,因此会在与您指定时间相差 6 小时的时刻触发。这种情况最浪费排查时间,因为大多数人首先执行的检查此时会通过。
设置工作流时区后,如果之后再修改 GENERIC_TIMEZONE,该工作流会忽略此修改。工作流中的时区值优先,并会持续生效,直到有人打开该工作流的设置。某个工作流在异常时间运行,而相邻工作流均正常时,几乎总是由此导致。
检查时钟,不要凭猜测
直接比较宿主机和容器的时间。
date
docker compose exec n8n date
docker compose exec n8n printenv TZ GENERIC_TIMEZONE设置 TZ 后,前两个命令应输出相同的实际时间。printenv 会为每个已存在的变量输出一行,因此输出两行表示两个变量都已设置,输出一行表示当前处于只修复了一半的状态。
现在从工作流内部询问 n8n 本身,因为容器 shell 无法告诉您工作流级别的时区是什么。在出现问题的工作流中添加一个 Code 节点,然后使用 Execute Workflow 运行一次。
return [
{
json: {
n8n_time: $now.toISO(),
n8n_zone: $now.zoneName,
system_time: new Date().toString(),
},
},
];n8n_zone 是此工作流的 Schedule Trigger 将使用的时区。它已经按照工作流优先于实例的顺序解析,因此可以直接回答这个问题。system_time 从 TZ 获取容器自身的时区。请在出现问题的工作流中运行此代码,而不是在新建工作流中运行,因为工作流级别的设置会随工作流一起保存。如果这两个值不一致,您无需打开任何配置文件就找到了问题。
Schedule Trigger 节点中的 Cron 表达式
Schedule Trigger 提供从秒到月的固定间隔,也提供 Custom (Cron) 选项,以满足其他间隔需求。Cron 表达式会根据工作流解析出的时区执行,因此 0 6 * * * 表示该时区的 06:00,而不是 UTC 的 06:00。将 crontab guru 中的五字段表达式直接粘贴即可。n8n 还接受可选的秒字段。文档中的字段表将其列在最前面:秒、分钟、小时、日、月、星期。
不要手动编码时区偏移。在 UTC 实例上写入 0 4 * * * 以达到柏林时间 06:00,只在冬季正确;整个夏季都会差 1 小时,因为柏林冬季为 UTC+1,夏季为 UTC+2。设置时区后,填写您实际需要的本地小时。
夏令时对设置为 02:30 的任务有什么影响
本地墙上时钟时间不保证对应唯一的时刻。每年有两次,一小时会消失,另一小时会重复;安排在这些时段内的任何任务都会受到影响。在任何 Linux 主机上都可以使用 date 观察这一现象,无需使用 n8n。
LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'date: invalid date '2027-03-28 02:30'这不是命令中的拼写错误。2027-03-28,柏林时间会从 02:00 直接跳到 03:00,因此当天不存在本地时间 02:30,date 无法将其转换为一个时刻。固定在本地时间 02:30 的任务没有可执行的时刻。相邻时间没有问题:date -d '2027-03-28 01:30' 会解析为 CET,date -d '2027-03-28 03:30' 会解析为 CEST。
秋季转换则相反。2027-10-31,柏林时间会从 03:00 回拨到 02:00,因此 02:30 会出现两次。
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CEST' '+%s'
LC_ALL=C TZ=Europe/Berlin date -d '2027-10-31 02:30 CET' '+%s'1824942600
1824946200这是两个不同的时刻,但它们都称为本地时间 02:30,间隔 3600 秒。固定在该时间的任务可能执行两次,也可能在无人选择的时间执行一次;这两种结果都不适合账单处理或备份轮换。请将计划移出这一时段。在大多数欧洲和北美时区,风险时段是本地时间 00:00 到 03:00。
以 UTC 安排基础设施,并向用户显示本地时间
时区需要解决两个不同的问题。机器需要稳定的时间间隔。用户需要易读的时间。
- 对于无人查看的任务,将工作流时区设为 UTC。备份、缓存预热、日志传输和报告生成都属于此类任务。UTC 没有夏令时,因此一年中的每一天,两次运行之间的间隔都完全等于您配置的间隔。
- 对于需要人工查看的任务,仍将计划保存在 UTC 中,并在显示时进行转换。一个表达式即可完成此操作:
{{ $now.setZone('Europe/Berlin').toFormat('yyyy-MM-dd HH:mm') }}会将本地时间写入消息正文,同时保持触发时间稳定。
同样的划分也适用于 n8n 之外的场景。当部分自动化任务作为 VPS 上的 systemd 服务和计时器运行时,其中的 OnCalendar 行会读取系统时区。系统时区是第四个时钟,也有自己的设置。将所有调度器统一设为 UTC 后,您只需记住一条规则,而不是四条规则。
对于需要汇总时间段的任务,这一点同样重要。因为 n8n AI agent 工作流在获取“昨天”的数据时,会根据解析所用的时区,静默地使用不同的 24 小时区间。
故障模式及您将看到的输出
所有任务的执行时间都偏移了约六小时。 未设置 GENERIC_TIMEZONE,因此使用内置默认值 America/New_York。docker compose exec n8n printenv GENERIC_TIMEZONE 完全没有输出。设置该变量,然后重新创建容器。
您编辑了 Compose 文件,但没有任何变化。 您运行了 docker compose restart,因此容器继续使用原有环境变量。运行 docker compose up -d,然后使用 docker compose exec n8n printenv TZ 确认。
触发器配置正确,但时间戳错误。 只设置了 GENERIC_TIMEZONE。Code 节点中的 new Date() 仍会从操作系统读取 UTC。将 TZ 设置为相同的值,然后重新创建容器。
某个工作流忽略了实例设置。 该工作流在自身设置中指定了时区,其优先级高于 GENERIC_TIMEZONE。打开画布,选择三个点、Settings、Timezone。
某个每日任务今年有一天执行了两次,或漏执行了一天。 其计划执行时间处于夏令时切换时段内。调整执行时间,或将该工作流改为使用 UTC。
FAQ
为什么我的 n8n schedule trigger 会在错误的小时触发?
工作流解析到的时区与您的预期不同。n8n 会优先使用工作流时区;如果工作流未设置时区,则使用 GENERIC_TIMEZONE 中的实例时区;如果两者都未设置,则使用内置默认值 America/New_York。如果自托管实例没有设置 GENERIC_TIMEZONE,计划任务会按纽约时间运行,而不是按 UTC 运行。因此,时间偏移通常不会与您所在时区相对于 UTC 的偏移一致。运行 docker compose exec n8n printenv GENERIC_TIMEZONE。没有输出表示该变量从未设置。
n8n 中的 TZ 和 GENERIC_TIMEZONE 有什么区别?
TZ 是容器内的操作系统时区。它决定容器内 date 返回的内容、容器日志行中显示的时间戳、Code 节点中 new Date() 返回的内容,以及您在容器内运行的任何脚本所读取的时区。GENERIC_TIMEZONE 是 n8n 实例时区,schedule 节点和 $now 等 Luxon 表达式会使用该时区。只设置其中一个,会导致触发时间正确但时间戳错误,或时间戳正确但触发时间相差数小时。请将两者设置为相同的值。
应该设置工作流时区,还是设置 GENERIC_TIMEZONE?
将 GENERIC_TIMEZONE 设置为整个实例的默认时区,仅在某个工作流确实属于其他时区时,才使用工作流级别的设置。工作流时区的优先级高于实例时区,且不会随后续的 GENERIC_TIMEZONE 更改而更新。因此,遗留的工作流级别覆盖设置可能在数月后仍难以排查。
时钟切换时,安排在 02:30 的任务会怎样?
该本地时间可能不存在,也可能出现两次。LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30' 返回 date: invalid date '2027-03-28 02:30',因为柏林时间在当天会从 02:00 跳到 03:00。2027-10-31,同一个墙上时钟读数会对应相隔一小时的两个时间点。请避免将计划任务安排在本地时间 00:00 到 03:00 之间,或者将工作流设置为 UTC,仅在人员查看时间时再转换为本地时间。