Engineering notes
Scheduling 11 min read

IANA timezones, DST, misfires, and overlap policies for SaaS schedules

Covers

Recurrence, IANA timezones, and DST

“Every day at 9” is a product promise, not a UTC interval. Model the promise first, then decide what should happen when time or execution gets awkward.

A recurring schedule is a promise about local time

“Run every 24 hours” and “run every day at 9am” only sound equivalent on quiet weeks in UTC. The first is elapsed time. The second is a wall-clock promise made to a person in a place.

Model that promise directly: frequency, interval, local time, timezone, start, and end condition. Store the next UTC instant as derived execution state, not as the source of truth for the recurrence.

Customer intent Store Do not reduce it to
Weekdays at 09:00 in Melbourneweekdays + 09:00 + Australia/MelbourneSunday/Thursday 22:00 UTC forever
Last day of each monthmonthly + last_dayday 31 with silent skips
First Monday every two monthsmonthly + first_monday + interval 2a fixed number of seconds
Stop after 12 occurrencesafter_occurrences: 12an estimated end date

This representation also gives you something intelligible to render back to the customer: “Every Monday and Friday at 9:00 AM Australia/Melbourne,” followed by the next few dates.

Use an IANA timezone, not a label or fixed offset

AEST is ambiguous and UTC+10 cannot tell you when an offset changes. An IANA identifier such as Australia/Melbourne names a ruleset with historical and future offset transitions.

Prefer the customer workspace’s timezone as a default, then let the creator confirm or change it. Do not silently substitute the browser timezone for an organization-wide report: the person configuring it may be travelling.

Choose what happens in the missing and repeated hour

Daylight-saving transitions create two edge cases. In spring, some local times do not exist. In autumn, some occur twice. There is no universally correct answer, so a scheduler needs a deterministic one and its preview should make that answer visible.

Case Reasonable choices Good default for customer operations
02:30 does not existSkip it, or move to the next valid instantMove forward for reports and reminders; document it
01:30 occurs twiceUse the first, use the second, or run twiceUse one deterministic occurrence, usually the first
Offset changes after creationHold UTC fixed, or hold local time fixedHold the customer’s local-time promise fixed

For money movement or compliance cutoffs, a generic default may be inappropriate. Restrict problematic times, require an explicit policy, or use a business calendar owned by the application. “Run twice because the clock did” is rarely a safe surprise.

A misfire is missed intent, not a failed attempt

A misfire happens when the scheduler notices an occurrence after its intended time—perhaps because the service was unavailable or the schedule was paused. A delivery retry is different: the occurrence exists, but the receiver did not accept it.

Policy Behavior Good fit
skipIgnore missed occurrences and continue from nowEphemeral reminders and freshness checks
run_onceCreate one recovery occurrenceReports and syncs where the latest result is enough
catch_upCreate the missed occurrencesLedger-like work where each period has meaning

Catch-up needs a bound. If a daily schedule wakes after a year, 365 simultaneous jobs are not a recovery plan. Set a maximum look-back window or batch the backlog, and show the operator what was condensed or skipped.

Overlap answers a different question: is the previous run still active?

Suppose a sync runs every five minutes but sometimes takes eight. The next occurrence is on time; it simply collides with work already in progress. Your choices are to allow concurrency, skip the new occurrence, or queue it behind the active run.

Policy Trade-off Typical use
allowLowest latency; requires concurrency-safe workIndependent notifications or snapshots
skipPrevents a backlog; some intervals produce no workRefresh jobs where only a newer result matters
queuePreserves every occurrence; backlog can growOrdered processing with meaningful periods

The scheduler cannot infer whether two executions conflict inside your domain. If you allow overlap, the action still needs idempotency and whatever locking protects shared records. If you skip, record a skipped run rather than pretending the occurrence never existed.

Preview the dates customers will actually get

Human-readable summaries are helpful, but examples are harder to misread. Show at least the next five local dates when a schedule is created or edited. In tests, choose a start date that crosses a real offset transition.

Preview across a transition ts
const preview = await tenantIncld.schedules.preview({
  recurrence: {
    frequency: "weekly",
    interval: 1,
    weekdays: ["sunday"],
    localTime: "02:30",
    timezone: "Australia/Melbourne",
  },
  count: 8,
  from: "2026-09-01T00:00:00Z",
})

for (const occurrence of preview.occurrences) {
  console.log(occurrence.local, occurrence.utc)
}

Cover these cases in fixtures rather than relying on the machine’s current timezone:

  1. 1 A weekly local time stays constant while its UTC offset changes.
  2. 2 A nonexistent local time resolves according to the documented rule.
  3. 3 A repeated local time produces one deterministic occurrence.
  4. 4 Day 31 behaves predictably in a shorter month.
  5. 5 Pausing across several occurrences follows the chosen misfire policy.
  6. 6 A slow active run follows the chosen overlap policy.

Preview recurrence with incld

incld stores daily, weekly, monthly, and one-time recurrence rules with an IANA timezone and returns local and UTC values from the preview API. Its recurrence engine moves a nonexistent local time to the next valid instant and chooses the first instant when a local time repeats. Monthly day-of-month schedules clamp to the last valid day.

Show that preview beside the human-readable summary when customers create or edit a schedule. Concrete dates make the timezone and recurrence easy to verify before saving.

Ship customer-facing schedules with the boundary intact.

incld hosts recurrence state, signed delivery, retries, history, and React UI. Your application keeps authorization, domain data, and the work itself.