← n8n Production Guides

GUIDE · Scheduling

n8n Cron & Timezone: Why Scheduled Workflows Fire at the Wrong Hour

2026-10-25 · William Ayad · 9 min read

Your Schedule Trigger says 9:00. It fired at 14:00 — or fired twice on the same night — or did not fire at all while the workflow was cheerfully Active. In nearly every case the expression was fine and the culprit is one sentence long: n8n evaluates schedules in the instance's timezone, not yours. Around that sentence orbit three smaller traps — downtime silently skipping runs, daylight-saving transitions doubling or skipping hours, and the draft/publish confusion of newer versions. This guide fixes all four, in the order they burn people.

Trap 1 — The timezone hierarchy (where the clock actually lives)

Three clocks compete for ownership of your schedule, and precedence matters:

The canonical self-hosted fix is one environment variable and a restart: GENERIC_TIMEZONE=Europe/Berlin (or Africa/Tunis, America/New_York — IANA names, not abbreviations; "EST" means different things in different libraries). Restart, then verify — do not trust the setting page, trust a firing: a test workflow scheduled for the next minute whose first node writes the current timestamp to a spreadsheet proves the zone the way no config dump can. On n8n Cloud the timezone is managed via instance settings — the verification ritual is identical.

Trap 2 — The expression you copied from Linux

n8n's Schedule Trigger uses the familiar 5-field cron grammar (minute hour day-of-month month day-of-week), and two recurring misunderstandings come from muscle memory. First, day-of-week conflict: when both day-of-month and day-of-week are restricted, classic cron semantics OR them — a schedule you read as "the 1st, if Monday" fires on the 1st and every Monday. Second, air-gapped ranges: expressions like 0 9-17 * * 1-5 are evaluated in the instance zone — in a UTC-default instance your "business hours" window silently slides for half the year as DST moves your local offset. Validate the expression against the instance zone, not your wrist.

A compact pre-solved table for the most-asked daily jobs (instance zone vs. what to write):

You wantInstance = your zone (recommended)Instance stuck on UTC
Daily 09:00, Berlin (CET, winter)0 9 * * *0 8 * * * (shifts +1h in summer — the DST tax of UTC schedules)
Weekdays 08:30, Tunis (UTC+1, no DST)30 8 * * 1-530 7 * * 1-5
First of month, 06:00, New York0 6 1 * *0 11 1 * * or 0 10 1 * * — offset changes with DST

Trap 3 — Downtime does not queue your runs

If the instance is down at 02:00, the 02:00 run does not happen at 02:03 when it recovers. Scheduled executions missed during downtime are skipped by default — after restart the workflow simply waits for the next matching time. For a nightly sync this means a silent missing day, which your users discover before you do unless an absence-of-signal alarm exists (the monitoring guide's third layer is built exactly for this). Structural defenses that actually work: make scheduled workflows idempotent (re-running yesterday does not corrupt anything), process "everything since last success" rather than "today" (a persisted watermark beats a calendar assumption), and for business-critical windows run a manual catch-up after every unplanned restart.

Trap 4 — DST: the night your daily workflow runs twice

This one is not folklore — it is tracked upstream as issue #42070: daily workflows in Europe/Berlin running twice on the autumn fall-back night, when the 02:00 hour exists two times in local time. The mirror image arrives in spring, when 02:30 does not exist at all and a schedule pinned to it simply skips. Any daily schedule pinned to the ambiguous band (00:00–03:00 local, zone-dependent) inherits this twice-yearly surprise. Two durable fixes: pin DST-sensitive daily jobs to UTC — a zone that never transitions — and move the remaining local-time jobs out of the 0–3 AM band. A 04:00 job never meets a duplicated hour.

"It never fired at all": the non-timezone checklist

When the schedule is innocent, the ladder from workflow-not-running applies — but two scheduling-adjacent rungs deserve emphasis here. First, publish state: current n8n versions save edits as drafts until published — "Active" with unpublished changes can mean the scheduler is still running the last published trigger configuration (or none), the draft/publish foot-gun documented in the not-running guide. Second, worker split: in queue mode the scheduler lives on the main process while execution happens on workers — a healthy schedule with starved workers looks like a dead schedule from the outside. Confirm registrations on main, capacity on workers, then blame the clock.

The five-minute verification ritual (run once, sleep forever)

  1. Read the instance zone from a firing, not a setting: timestamp-writing test workflow, scheduled one minute out.
  2. Set GENERIC_TIMEZONE + TZ to the IANA zone, restart, re-fire the test.
  3. Audit every daily workflow: anything pinned 00:00–03:00 local in a DST zone gets re-pinned to UTC or moved to 04:00+.
  4. Mark DST week in the ops calendar — the transitions are dates, and they are knowable in advance (last Sunday of March and October in Europe).
  5. Add a missing-day alarm: no successful execution by window-end = alert, per the absence-of-signal pattern.

Hands-on: confirm the running zone in one command

Config files lie by omission all the time — the truthful answer comes from the running process. On a Docker deployment, docker exec <container> env | grep -E 'TIMEZONE|^TZ=' shows what the container actually received (absence of GENERIC_TIMEZONE after a "we set it" claim is depressingly common — a compose file edited but never re-deployed applies nothing). Stack it with the firing test from the ritual above and you have ground truth from both ends: the environment the process sees, and the wall-clock result the scheduler produces.

When "wrong hour" is a migration symptom

The nastiest timezone regressions arrive with infrastructure moves: a Docker image redeployed on a new host, a restore onto a fresh cloud instance, a Helm chart whose env section dropped a line. The workflows did not change; the zone silently reverted to UTC, and a week of shifted schedules follows before anyone correlates it with the migration. The durable defense is the same discipline the backup guide applies to data: environment belongs in infrastructure-as-code — the zone variable lives in version control next to the compose file, and any restore drill ends with the one-command zone check, not with "looks up".

Reading your own schedule like an operator

Two habits close the loop. First, know which mode each trigger is in: the Schedule Trigger's interval mode (every N minutes/hours/days) is immune to wall-clock confusion by construction — "every 6 hours" has no timezone to get wrong — while custom cron expressions carry all of it. Migrating jobs that never truly needed clock time into interval mode eliminates the entire bug class for them. Second, read the Executions history as a schedule fingerprint: a daily job's timestamps should form one column at one hour; a column that drifts, doubles, or gaps is the timezone/DST/downtime story telling itself in data before any user complains. Checking that column after every infrastructure touch is about thirty seconds of habit.

If you want the checks pre-built instead of memorized

The discipline above is every-word teachable — that is what the blog is for. The same rules also exist pre-assembled in the n8n Reliability Kit (one-time $19 launch, disclosing plainly: mine), where time-related risk is part of the operational layer: decision trees walk "fires at the wrong hour / never fires / fires twice" to their tier-by-tier causes; the documented failure library carries the DST double-fire and skipped-window classes with incidence context; the 34-point preflight checklist folds the verification ritual (zone firing test, DST-band audit, watermark patterns) into launch-day review; and the production blueprints ship scheduled-workflow skeletons with idempotency and catch-up windows already wired. All of it runs in your browser on copies of your exports — the same local-only architecture as the free tools, with the honest boundary that no file-based tool can read your server's live timezone: that firing test above is yours to run, once, and then the kit keeps the rest of the discipline from decaying.

And when a "never fired" complaint is actually an activation or wiring trap rather than a clock trap, the free audit flags the structural suspects (inactive-state shapes, environment-local URLs, missing retries) on the exported JSON in seconds — free, local, and deliberately narrower than the kit.

Frequently asked questions

Why does my cron expression run at the wrong time?

It is evaluated in the instance timezone, not your laptop's. 0 9 * * * means 09:00 wherever the instance believes it is — often UTC on self-hosted defaults. Set GENERIC_TIMEZONE to your IANA zone, restart, and verify with a timestamp-writing test run.

What happens to schedules while the server is down?

They are skipped — n8n does not replay missed windows after restart. Design for idempotency, track "since last success" instead of "today", and catch up critical windows manually.

Why did my daily workflow run twice on DST night?

Fall-back makes one local hour exist twice; local-time schedules in the ambiguous band can fire on both occurrences (tracked as issue #42070). Pin daily jobs to UTC or move them out of the 00:00–03:00 local band.

Does the Schedule Trigger follow the same rules as the old Cron node?

Yes — the Schedule Trigger is the unified, modern replacement for the legacy Cron node, and both evaluate against the same instance clock. Migrating one to the other changes the UI, not the timezone math:GENERIC_TIMEZONE, workflow overrides, downtime behavior and DST all carry over. Re-verify the firing hour after any such migration with the timestamp test.

Is UTC the right GENERIC_TIMEZONE for everyone?

For multi-region teams: yes — UTC never transitions and every log aligns; the price is expressing "9AM Monday" as a UTC hour that shifts locally twice a year. Single-region: pin your real zone explicitly, and re-pin only the DST-fragile daily jobs to UTC.

The one-paragraph answer to bookmark

Scheduled triggers obey the instance clock; be sure there is one — set GENERIC_TIMEZONE to an IANA zone (and reboot), verify by firing, keep daily jobs out of the DST-ambiguous midnight band or on UTC, assume downtime skips rather than queues, and guard every business-critical window with a missing-run alarm. Wrong-hour schedules are a misconfiguration story, skipped windows an architecture story — both end the same week you run the five-minute ritual.

The preflight that never forgets a DST band or a missing watermark

Decision trees + failure library + 34-point checklist + blueprints — one-time $19, in your browser (disclosing: ours).