SSD Nodes Learn 🎉 VPS from $5.50/mo
How to do am Matt ConnorBy Matt Connor

Why n8n Schedule Trigger Dey Fire Wrong Time

n8n self-hosted dey read TZ, GENERIC_TIMEZONE and workflow timezone. Set the three correctly, or your Schedule Trigger fit fire hours away from the time wey you choose.

Why n8n schedule trigger dey fire for wrong hour

n8n schedule trigger dey fire for wrong hour because n8n dey read timezone from three separate places, and when you fix one, na only part of the problem e fix. The three places na the container own TZ variable, the instance default GENERIC_TIMEZONE, and timezone wey you set inside one workflow. Set all three once, and every schedule wey you write afterwards go run for the time wey you expect.

First, correct one wrong assumption. Fresh self-hosted n8n no dey schedule with UTC (coordinated universal time). Container clock na UTC because official image no set any TZ. Scheduling na separate layer, and n8n documented default for GENERIC_TIMEZONE na America/New_York (as of August 2026). So, instance wey nobody configure go fire Schedule Triggers with New York time. Na why the offset wey people report rarely match their own distance from UTC. Owner for Berlin wey request 06:00 go get 12:00 local time, and 11:00 during the weeks for March when United States don move to daylight saving time but Europe never move.

Timezone layer dem three, and which one go win

TZ na operating system timezone wey dey inside the container. n8n documentation describe am as the variable wey set system timezone to control wetin scripts and commands like date go return. E decide wetin date go print inside the container, which timestamp go enter container log line, wetin new Date() go return for a Code node, and wetin any shell script wey you run there go see. E no affect when Schedule Trigger go fire.

GENERIC_TIMEZONE na n8n instance timezone. Documentation call am n8n instance timezone and note say e important for schedule nodes like Cron. Cron for here mean the standard time-based scheduling syntax, and n8n expose am as the Custom (Cron) option for Schedule Trigger.

You set workflow timezone separately for each workflow. Open the workflow for the canvas, select the three dots for the upper right corner, select Settings, then change the Timezone value. E override GENERIC_TIMEZONE for that particular workflow.

For Schedule Trigger, the order no dey change. n8n use workflow timezone if the workflow get one. If e no get one, n8n use the instance timezone from GENERIC_TIMEZONE. If that one no dey, e use the built-in default of America/New_York. TZ no dey involved for any step of that decision.

For dates inside your nodes, the answer depend on which clock the code ask for. Luxon, the date library wey n8n expressions use, use n8n timezone. So $now and $today follow the same workflow-then-instance order wey the trigger use. Plain JavaScript new Date() for a Code node ask the operating system, so e follow TZ. Na this split cause most of the confusion: the trigger fit dey correct while every timestamp wey the workflow write dey off by some hours.

Set all three for the Compose file

Put TZ and GENERIC_TIMEZONE side by side for the file, so nobody go set one and forget the other. The fragment below na the timezone part of a working service. The rest of the file, the reverse proxy and the certificate, come from a self-hosted n8n for VPS wey dey behind HTTPS.

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:

Apply am with docker compose up -d, no be with docker compose restart. Restart dey start the same container again with the environment wey e get when dem create am, so the file changes but the running process no change. up -d dey see the changed environment and recreate the container. If you keep these values for an env file instead of inline, the same recreate rule still apply, and the Compose env file and secrets handling guide explain where dem dey read that file from.

Use an IANA (Internet Assigned Numbers Authority) zone name for Region/City form, like Europe/Berlin or America/Sao_Paulo. Those names carry the daylight saving rules for that place, so the offset dey change when the local clocks change. A fixed-offset name like Etc/GMT+5 never change with the seasons, and the sign dey opposite to wetin you fit guess. Run LC_ALL=C TZ=Etc/GMT+5 date +%z and e go print -0500. Avoid those names.

Why setting only one of dem leave you half fixed

Set GENERIC_TIMEZONE alone, and the Schedule Trigger go fire for the hour wey you want, while everything wey dey read the operating system go remain for UTC. A Code node wey dey call new Date().toString() go return UTC string, container log lines go get UTC timestamp, and any file name wey system clock build go change date for wrong midnight.

Set TZ alone, and the opposite go happen. docker compose exec n8n date go print your local time, so e go look like say everything work, while the Schedule Trigger still dey use America/New_York and go fire six hours away from the hour wey you request. Na this version dey waste pass time, because the first check wey most people run go now pass.

Set a workflow timezone, then change GENERIC_TIMEZONE later, and that workflow no go follow the change. The workflow value get priority, and e go keep that priority until somebody open the workflow settings. If one workflow dey run for strange hour while the ones around am dey okay, na almost always this one.

Check the clocks instead of guessing

Compare the host and the container directly.

date
docker compose exec n8n date
docker compose exec n8n printenv TZ GENERIC_TIMEZONE

The first two commands suppose print the same wall-clock time once TZ don set. printenv dey print one line for every variable wey dey exist, so two lines of output mean say both don set, and one line mean say na the half-fixed state you dey look at.

Now ask n8n itself from inside a workflow, because container shell no fit tell you the timezone wey dey apply to the workflow. Add a Code node to the workflow wey dey misbehave, then run am once with Execute Workflow.

return [
  {
    json: {
      n8n_time: $now.toISO(),
      n8n_zone: $now.zoneName,
      system_time: new Date().toString(),
    },
  },
];

n8n_zone na the zone wey this workflow's Schedule Trigger go use. E don already resolve am through the workflow-then-instance order, so e answer the question directly. system_time carry the container's own zone from TZ. Run am for the workflow wey dey misbehave, no be fresh one, because the workflow-level setting dey travel with the workflow. If those two values no match, you don find the problem without opening even one config file.

Cron expressions for Schedule Trigger node

Schedule Trigger dey offer fixed intervals from seconds reach months, plus Custom (Cron) for anything wey dem no cover. Cron expression dey read according to the workflow resolved timezone, so 0 6 * * * mean 06:00 for that zone, no be 06:00 UTC. You fit paste five-field expression from crontab guru as e be. n8n still accept optional seconds field, and documentation field table dey put am first: second, minute, hour, day of month, month, day of week.

No ever encode offset by hand. To write 0 4 * * * for UTC instance so e reach 06:00 for Berlin correct only for winter, but e go one hour wrong throughout summer, because Berlin dey run UTC+1 for winter and UTC+2 for summer. Set the zone, then write the local hour wey you really mean.

Wetin daylight saving dey do to job wey set for 02:30

Local wall-clock time no be guaranteed instant. Twice every year, one hour dey disappear and another hour dey repeat, and any job wey schedule inside those hours go dey affected. You fit watch am happen with date for any Linux box, without n8n.

LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30'
date: invalid date '2027-03-28 02:30'

That one no be typo for the command. On 2027-03-28, Berlin clock go jump directly from 02:00 to 03:00, so 02:30 local no dey exist that day, and date no fit turn am into an instant. Job wey anchor to 02:30 local no get time to run. The times wey dey near am dey okay: date -d '2027-03-28 01:30' resolve as CET and date -d '2027-03-28 03:30' resolve as CEST.

The autumn transition na the opposite. On 2027-10-31, Berlin clock go move backward from 03:00 to 02:00, so 02:30 go happen two times.

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

Na two different instants, and both dem dey call 02:30 local, with 3600 seconds between dem. Job wey pin for that time fit run two times, or run once for an hour wey nobody choose. Neither result good for billing run or backup rotation. Move the schedule comot from that window. For most European and North American zones, the risky time na 00:00 to 03:00 local.

Plan infrastructure for UTC and show local time to people

The standard answer dey separate the two jobs wey timezone dey do. Machines need one stable interval. People need readable time.

  • For work wey nobody dey watch, set workflow timezone to UTC. Backups, cache warming, log shipping and report generation dey here. For UTC, gap between two runs na exactly the gap wey you write, every day for the year, because UTC no get daylight saving.
  • For work wey person go read, keep schedule for UTC and convert am when you dey display am. One expression dey do am: {{ $now.setZone('Europe/Berlin').toFormat('yyyy-MM-dd HH:mm') }} go put local time for message body while trigger remain stable.

The same split dey apply outside n8n. When part of your automation dey run as systemd service and timer for the VPS, e OnCalendar line dey read system timezone, and that one na fourth clock with e own setting. If you keep every scheduler for UTC, na one rule you go remember instead of four. E matter for anything wey dey summarise one period too, because n8n AI agent workflow wey you ask for yesterday numbers go quietly use different 24 hours, depending on which zone resolve am.

Wey things dey fail and wetin you go see

Everything dey happen about six hours off. GENERIC_TIMEZONE no set, so built-in default America/New_York dey apply. docker compose exec n8n printenv GENERIC_TIMEZONE no print anything. Set am, then recreate the container.

You edit the Compose file but nothing change. You run docker compose restart, so the container keep im original environment. Run docker compose up -d, then confirm with docker compose exec n8n printenv TZ.

The trigger correct but the timestamps wrong. Na only GENERIC_TIMEZONE set. A new Date() for a Code node still dey read UTC from the operating system. Set TZ to the same value, then recreate.

One workflow no follow the instance setting. That workflow get im own timezone for settings, and e override GENERIC_TIMEZONE. Open the canvas, three dots, Settings, Timezone.

A daily job run two times, or skip one day, once this year. The scheduled hour dey inside a daylight saving transition. Move the hour, or move that workflow to UTC.

FAQ

Why my n8n schedule trigger dey fire for wrong hour?

The workflow dey resolve timezone wey different from the one you assume. n8n go pick the workflow timezone if the workflow get one. If e no get one, e go use the instance timezone from GENERIC_TIMEZONE. If that one no dey set, e go use the built-in default of America/New_York. Self-hosted instance wey nobody set GENERIC_TIMEZONE go schedule with New York time, no be UTC. Na why the offset rarely match your own distance from UTC. Run docker compose exec n8n printenv GENERIC_TIMEZONE. If command no return output, e mean say dem never set am.

Wetin be the difference between TZ and GENERIC_TIMEZONE for n8n?

TZ na the operating system timezone inside the container. E control wetin date return inside the container, the timestamps wey show for container log lines, wetin new Date() return for a Code node, and wetin any script wey you run inside there go see. GENERIC_TIMEZONE na the n8n instance timezone. Schedule nodes and Luxon expressions like $now dey use am. If you set one without the other, you fit get correct trigger with wrong timestamps, or correct timestamps with trigger wey dey fire hours away. Set both to the same value.

Make I set the workflow timezone or GENERIC_TIMEZONE?

Set GENERIC_TIMEZONE as the default for the whole instance. Use the per-workflow setting only where one workflow truly belong to another zone. The workflow value dey override the instance value. E no go follow later changes to GENERIC_TIMEZONE. So, per-workflow override wey person forget fit hard to track down months later.

Wetin go happen to job wey dem schedule for 02:30 when clocks change?

That local time fit disappear or happen two times. LC_ALL=C TZ=Europe/Berlin date -d '2027-03-28 02:30' returns date: invalid date '2027-03-28 02:30' because the Berlin clock jump from 02:00 go 03:00 that day. On 2027-10-31, the same wall-clock reading go map to two instants wey one hour dey between dem. Keep scheduled work outside the 00:00 to 03:00 local time band, or set the workflow to UTC and convert am to local time only where person go read am.