Timezone handling for scheduled social posts, done right
The classic scheduler bug: a post scheduled in the customer’s timezone fires at the wrong hour. How it happens and how to handle timezones correctly end to end.
Scheduled posts fire at the wrong hour when a tool stores a naive local time and interprets it in the server's timezone (usually UTC), or freezes a UTC offset that goes stale after a daylight saving transition. The fix: resolve the customer's wall-clock time plus IANA timezone into one fixed UTC instant at creation, and store that instant.
A marketer in São Paulo schedules a post for 9:00am. It fires at noon. Or at 8:00am, three weeks later, after a daylight saving transition nobody on the team thought about. This is one of the oldest bugs in scheduling software, and social publishing tools re-introduce it constantly, because the naive implementation looks correct in every manual test and only breaks for specific customers, at specific times of year, in specific timezones.
It is also one of the few bugs that erodes trust disproportionately to its apparent size. A post firing three hours off is not a crash or an error message — it is a silent, confident wrong answer, and the customer usually finds out from the timestamp on a live post, not from the tool.
PostGate handles OAuth, publishing and delivery evidence for every network through one API. Start free or read the docs.
The bug, precisely
The failure has one root cause with two common shapes. Either the tool captures "9:00am" as a naive local time with no timezone attached, and later interprets that string using whatever timezone the server happens to be running in — which is UTC on almost every hosting platform. Or the tool does capture a timezone, but re-resolves the local time against the current UTC offset for that timezone every time it reads the schedule, instead of fixing the offset once, at the moment the schedule was created.
Both shapes look identical in a demo, because a demo is usually run once, close to the scheduled time, without crossing a daylight saving boundary in between. The bug only surfaces weeks later, for a real customer, in a timezone the developer was not testing in.
The correct model: resolve once, store an instant
The fix is to treat scheduling as a one-time resolution problem, not a display-time problem. At the moment a schedule is created, combine the customer's local wall-clock time with their IANA timezone — America/Sao_Paulo, not a raw UTC offset — and resolve that pair into a single, explicit UTC instant. That instant is what gets stored and what actually fires. It is never recomputed later against "whatever this timezone currently means," because by delivery time that recomputation may already be wrong.
PostGate's post-scheduling model reflects this directly: an ISO 8601 scheduled_at value publishes at that fixed instant, full stop. Recurring schedules take the same approach but at template level — a cron expression is interpreted in the supplied IANA timezone at the moment each occurrence materializes, using the timezone's real rules for that specific date rather than a UTC offset frozen at creation time.
# One-off: resolved once, fires at exactly this instant
{
"post": { "body": "Doors open in one hour." },
"profiles": ["pro_instagram123"],
"scheduled_at": "2026-09-10T12:00:00.000Z"
}
# Recurring: timezone-aware, re-evaluated per occurrence
{
"name": "Weekday update",
"kind": "cron",
"expression": "0 9 * * 1-5",
"timezone": "America/Sao_Paulo",
"template": { "post": { "body": "Good morning from Acme." }, "profiles": ["pro_linkedin456"] }
}Why do naive implementations break at DST transitions?
A UTC offset is not a property of a timezone — it is a property of a timezone on a given date.America/New_York is UTC-5 for part of the year and UTC-4 for the rest. Code that captures "9:00am America/New_York" and quietly converts it to a fixed UTC offset at creation time will drift by an hour the moment the calendar crosses into or out of daylight saving — every recurring post fires an hour early or an hour late until someone notices and manually corrects it.
The one-off case has a second trap: the wall-clock time itself can be ambiguous or nonexistent around a transition. When clocks move forward, a local time like 2:30am on the transition date never happens at all; when they move back, it happens twice. A scheduler that resolves the instant once, using the timezone's actual rules for that specific date, handles this correctly by construction. A scheduler that treats "9am" as portable across time never notices it was wrong until the customer does.
This is also why storing a recurring template's timezone matters as much as storing the expression: PostGate re-evaluates 0 9 * * 1-5 against America/Sao_Paulo's real rules every time it materializes an occurrence, rather than freezing an offset once and letting it go stale.
What should you check if this has already bitten you?
If a tool has ever fired a scheduled post at the wrong hour, look for two symptoms specifically: whether the drift is a clean, fixed number of hours that matches the difference between the customer's timezone and UTC (a naive-server-time bug), and whether the drift only appeared after a specific date that lines up with a daylight saving transition (a stale-offset bug). Both point to the same root cause — a local time that was never resolved into a fixed instant using the timezone's actual rules — just caught at different points in the calendar.
Frequently asked questions
Why did my scheduled post publish at the wrong time?
Look for two symptoms. If the drift is a clean, fixed number of hours matching the difference between your timezone and UTC, the tool stored a naive local time and interpreted it in server time. If the drift only appeared after a date that lines up with a daylight saving transition, the tool froze a stale UTC offset at creation time. Both come from never resolving the local time into a fixed instant.
Should scheduled post times be stored in UTC or local time?
For one-off posts, combine the local wall-clock time with the IANA timezone at the moment the schedule is created, resolve that pair into a single explicit UTC instant, and store the instant — never recompute it later. Recurring schedules are different: store the cron expression plus the IANA timezone, and re-evaluate the expression against that timezone each time an occurrence materializes.
Why use an IANA timezone name instead of a UTC offset?
Because a UTC offset is not a property of a timezone — it is a property of a timezone on a given date. America/New_York is UTC-5 for part of the year and UTC-4 for the rest. Code that converts a local time to a fixed offset at creation time drifts by an hour once the calendar crosses a daylight saving boundary.
How should recurring schedules handle daylight saving transitions?
By re-evaluating the cron expression against the timezone’s real rules every time an occurrence materializes, rather than freezing an offset once. PostGate interprets an expression like 0 9 * * 1-5 in the supplied IANA timezone at the moment each occurrence is generated, so a 9:00am post stays at 9:00am local time across DST changes.
What happens if a scheduled time falls inside a DST transition?
The wall-clock time can be ambiguous or nonexistent: when clocks move forward, a local time like 2:30am on the transition date never happens at all, and when they move back, it happens twice. A scheduler that resolves the instant once, using the timezone’s actual rules for that specific date, handles both cases correctly by construction.
