The Workflow That Ran at 3 AM: Timezone Bugs in Scheduled n8n Workflows
This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support — the mistakes are mine, the fixes are tested. The report went
This is part of my build-in-public series on running LLM workflows in n8n in production. Everything here is from real templates I sell and support — the mistakes are mine, the fixes are tested.
The report went out at 3 AM. Nobody was awake to read it.
A client workflow sent a daily summary every morning at 9 AM. It ran perfectly for months, and then one Monday the summary arrived at 3 AM, woke nobody up, and sat unread until noon — by which time the numbers in it were already stale. Nothing had been edited. No node had changed. The schedule was still "9 AM".
What had changed was the clock underneath it. The server ran in UTC, the business ran in another timezone, and a daylight saving transition moved the two apart by an hour. The workflow had never been scheduled for 9 AM local time. It had been scheduled for 9 AM server time, and for months those two happened to agree.
Timezone bugs in scheduled workflows are quiet in exactly the way production bugs love: the workflow runs, it succeeds, it produces output — just at the wrong hour, for the wrong day, against the wrong date boundary. Nobody gets an error. Somebody gets a report about yesterday when they needed one about today.
Rule 1: a schedule without a timezone is a guess
Every scheduled workflow needs an explicit answer to one question: 9 AM where? The cron expression alone does not answer it. A server in UTC, a hosted instance in another region, and a business team in a third timezone will each read "9 AM" differently, and the schedule will silently follow whichever clock the executor happens to use.
So write the timezone into the schedule itself, in the same place as the cron expression, not in a comment somewhere else. If the scheduling tool lets you pin a timezone, pin it. If it does not, convert the cron expression to the executor's timezone and write down both the local time and the converted time, so the next person who reads it can see the mapping instead of having to reverse-engineer it.
And never assume the executor's timezone is stable. Hosted platforms move workloads, containers restart in new regions, and a migration that changes nothing about your workflow can still shift every scheduled run by hours. A schedule that only works because the server happens to be in the right timezone is not scheduled — it is lucky.
Rule 2: store UTC, display local, never mix the two
Inside the workflow, dates should live in UTC from the moment they enter until the moment they leave. The instant a record is created, convert it to UTC and keep it there. The instant a date is shown to a human — in an email, a report, a Slack message — convert it to that human's timezone at the display step, and only there.
The bug appears the moment a workflow does date math in local time: "yesterday" computed in one timezone is a different set of records than "yesterday" in another, and a filter like "created after midnight" means midnight somewhere. A daily summary that filters on local midnight will either double-count or drop a slice of records at the boundary, depending on which side of the line the server sits on.
The rule is boring and absolute: UTC in storage and in filters, local time only at the edge where a human reads it. Any node that formats a date for display should be the last node that touches that date, not the first.
Rule 3: test the daylight saving transition, not just the happy path
Most timezone bugs do not show up on an ordinary Tuesday. They show up twice a year, on the weekend the clocks change — when a 9 AM run becomes 8 AM or 10 AM, when a day has 23 or 25 hours, and when a "daily" job that runs every 24 hours slowly drifts against the wall clock.
So test the transition explicitly. Pick the next daylight saving change for every timezone your workflow touches, and walk through what the schedule does on that weekend: does the run still land at the intended local hour, does the daily boundary still split the right days, does a job scheduled during the repeated hour run once or twice? The repeated hour is the nastiest case — a time that occurs twice in one day — and a scheduler that fires on wall-clock time can fire the same job twice without any error.
If your workflow has no test for the transition, it has a bug scheduled for the next clock change. You just do not know which one yet.
Rule 4: make the date boundary visible in the output
A daily report should say which day it covers, in which timezone, in its first line — not buried in a footnote, not implied by the send time. "Daily summary for 2026-10-31 (Asia/Manila)" is a claim a reader can check. "Daily summary" is a claim nobody can verify until the numbers look wrong.
This matters because the date boundary is where timezone bugs become data bugs. A report generated at 1 AM local time that filters on UTC midnight covers a different day than the reader expects, and the discrepancy is exactly one hour of records — small enough to look plausible, large enough to matter. Stating the covered window turns a silent off-by-one into a visible mismatch the first time someone compares the report against the source system.
The same applies to any workflow that batches by day: state the window, in the timezone the reader uses, every time. If the workflow cannot state its window confidently, it does not understand its own boundary.
Rule 5: alert on schedule drift, not just on failure
A workflow that runs at the wrong hour does not fail, so failure alerts will never catch it. What catches it is an alert on the run time itself: if a job that should run at 9 AM local runs outside a window around that hour, page someone. If a daily job produces no output by a cutoff time, page someone — silence is also a signal, and a job that never ran looks exactly like a quiet day.
This is the same discipline as elsewhere in this series: green does not mean correct. A scheduled workflow needs two kinds of monitoring — did it run, and did it run when it was supposed to. The second check is the one that catches timezone drift, and it costs one comparison against the expected local hour.
The checklist I actually use
- Schedule pinned to an explicit timezone, with the local time and the executor time both written down.
- All stored dates and all filters in UTC; local time only at the display edge.
- Daylight saving transitions tested for every timezone the workflow touches, including the repeated hour.
- Every daily output states its covered window and timezone in its first line.
- An alert on run time drift, not just on failure — and an alert when an expected run never appears.
It is unglamorous work, and no part of it makes a workflow faster. But it is the difference between a report that arrives when someone can act on it and a report that arrives at 3 AM, unread — and in production, that difference is the whole point of scheduling anything at all.
I'm building AI Automation Lab — production-ready n8n workflow templates. The free AI Hook Generator shows how I structure real workflows; my open-source templates live at github.com/zhp910318/n8n-ai-automation-lab.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.