Skip to main content

⏱️ Cron Timezone and DST Behavior by Platform

CronForge — visual crontab editor with timezone + DST

Every scheduler evaluates cron expressions in some timezone, and most of them do not tell you which one until a job fires an hour late. This page collects the answer for 19 platforms: the timezone each one uses, whether you can change it, and what happens to a schedule that lands in the hour daylight saving time skips or repeats. Every cell links to the upstream documentation it came from, and where upstream is silent, the cell says so rather than guessing. The builder above is preloaded with 30 2 * * 0 — the expression most likely to bite you.

Expression

At 02:30 on Sunday.
minute · hour · day-of-month · month · day-of-week   (5 fields, 24h clock)

Presets

Visual editor click cells to toggle

Minute (0-59)
Hour (0-23)
Day of month (1-31)
Month (1-12)
Day of week (0-6, Sun=0)

Next 10 fire times

    Why 02:30 is the expression that breaks

    On the night the clocks go forward, 02:30 does not exist. On the night they go back, it happens twice. A job scheduled for 30 2 * * 0 therefore either silently skips a week or runs twice in an hour, and which one you get depends entirely on the scheduler — not on cron itself.

    This is not an edge case that shows up in testing. It shows up once or twice a year, in production, usually on a billing or reconciliation job where running twice is worse than not running at all. The fix is always the same: know which timezone your scheduler uses before you pick the hour.

    Timezone and DST behavior by platform

    Timezone and DST behavior by platform. Last verified 25 July 2026.
    PlatformSchedule evaluated inTimezone configurableSpring forward (skipped hour)Fall back (repeated hour)
    Unix cron (cronie / Vixie)System local timeYes — CRON_TZ= or TZ= line above the jobJobs in the skipped interval run immediately after the shiftNot run a second time
    GitHub ActionsUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions
    GitLab CI scheduled pipelinesUTC by defaultYes — cron_timezone per scheduleNot documented upstreamNot documented upstream
    Kubernetes CronJobkube-controller-manager's local timeYes — .spec.timeZoneNot documented upstreamNot documented upstream
    Vercel Cron JobsUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions
    Netlify Scheduled FunctionsUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions
    Cloudflare Workers Cron TriggersUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions
    AWS EventBridge SchedulerUTC by defaultYes — ScheduleExpressionTimezone (IANA)Not documented upstreamNot documented upstream
    Azure Functions timer triggerUTC by defaultYes — WEBSITE_TIME_ZONE app settingAdjusted automatically when WEBSITE_TIME_ZONE is setAdjusted automatically when WEBSITE_TIME_ZONE is set
    pg_cron (incl. Supabase)GMT by defaultYes — cron.timezone in postgresql.confNot documented upstreamNot documented upstream
    Apache AirflowConfigured default (often UTC)Yes — timezone-aware DAG start dateCron schedules keep local time; timedelta schedules do not adjustCron schedules keep local time; timedelta schedules do not adjust
    Quartz SchedulerScheduler defaultYes — CronTrigger timezoneA fire time in the skipped hour does not occurA fire time in the repeated hour can occur twice
    Spring @ScheduledScheduler's timezoneYes — zone attributeNot documented upstreamNot documented upstream
    Celery BeatUTC by defaultYes — timezone setting (any ZoneInfo zone)Not documented upstreamNot documented upstream
    Jenkins (build periodically)Controller's system timezoneYes — TZ=Area/City line in the triggerNot documented upstreamNot documented upstream
    systemd timersSystem local timeYes — timezone suffix in OnCalendar=See notes — edge cases are tracked upstreamSee notes — edge cases are tracked upstream
    Render cron jobsUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions
    Railway cron jobsUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions
    Fly.io MachinesUTCNoN/A — UTC has no transitionsN/A — UTC has no transitions

    Last verified 25 July 2026. Each row links to the upstream documentation it was read from. Where a platform does not document its behavior in the skipped or repeated hour, the cell says not documented upstream — that is a finding, not an omission on our part, and it is the reason the safe advice is to avoid scheduling inside the transition window at all.

    Expression format by platform

    Timezone is only half the problem. The same expression can be valid on one platform, invalid on the next, and valid-but-different on a third — six-field formats shift every position, and day-of-week numbering is not consistent across vendors.

    Expression format by platform. An expression that is valid on one row is frequently invalid on the next. Where no platform limit is documented, the shortest interval shown is simply the granularity of the expression format.
    PlatformFieldsSecondsNon-standard syntaxShortest interval
    Unix cron (cronie / Vixie)5No@reboot, @daily and friends; ranges, steps, names1 minute
    GitHub Actions5NoPOSIX only5 minutes
    GitLab CI scheduled pipelines5NoStandard cronDepends on instance limits
    Kubernetes CronJob5NoPOSIX; TZ/CRON_TZ in the schedule is rejected1 minute
    Vercel Cron Jobs5NoNo MON/JAN names; day-of-month and day-of-week cannot both be set1 minute (Hobby: once per day)
    Netlify Scheduled Functions5NoExtensions supported except @reboot and @annually1 minute
    Cloudflare Workers Cron Triggers5NoMost Quartz-style extensions, e.g. L, LW1 minute
    AWS EventBridge Scheduler6 (adds year)No?, L, W, #; 1 = Sunday numbering1 minute
    Azure Functions timer trigger6 (seconds first) or 5YesNCRONTAB1 second
    pg_cron (incl. Supabase)5Yes, separately'[1-59] seconds' interval syntax; cannot mix with 5 fields1 second
    Apache Airflow5NoPresets (@daily), custom timetables1 minute
    Quartz Scheduler6 or 7 (seconds first, optional year)Yes?, L, W, #1 second
    Spring @Scheduled6 (seconds first)YesMacros such as @daily; L, # in CronExpression1 second
    Celery Beat5Nocrontab(), solar(), timedelta1 second (via timedelta)
    Jenkins (build periodically)5NoH hash operator, including H(0-7) ranges1 minute
    systemd timersCalendar spec, not cronYesOnCalendar=, Persistent=, RandomizedDelaySec=1 second
    Render cron jobs5NoStandard cron1 minute
    Railway cron jobs5NoStandard cron5 minutes
    Fly.io MachinesNo cron expressionsNoschedule = hourly, daily, weekly or monthly1 hour

    Platform notes and sources

    The detail behind each row, with the documentation it came from. These are stable anchors — link to a specific platform with #tz-kubernetes, #tz-airflow and so on.

    Unix cron (cronie / Vixie)

    The special DST handling applies only to local time changes of less than three hours, and only to jobs that run at a specific time with a granularity coarser than one hour. Anything running more often than hourly is scheduled normally, which is why */5 * * * * is unaffected by DST but 30 2 * * * is not.

    Source: crontab(5)

    GitHub Actions

    Because the schedule is fixed to UTC, a workflow you set for 09:00 local time drifts to 08:00 or 10:00 twice a year. There is no timezone setting to fix this — you either accept the drift or change the expression at each transition. Two further constraints worth knowing: runs can be delayed during periods of high load, and in a public repository scheduled workflows are disabled automatically after 60 days without repository activity.

    Source: GitHub Docs — events that trigger workflows

    GitLab CI scheduled pipelines

    The timezone is set per schedule (cron_timezone in the API, --cronTimeZone in glab), so local wall-clock time is preserved across a transition. What happens to a schedule that lands inside the skipped or repeated hour is not stated in the documentation — if that matters for your job, do not schedule it between 01:00 and 03:00.

    Source: GitLab Docs — scheduled pipelines

    Kubernetes CronJob

    If .spec.timeZone is unset, the schedule is interpreted in the timezone of the kube-controller-manager process — which means the same manifest fires at different wall-clock times on two clusters. Putting TZ= or CRON_TZ= inside .spec.schedule is not supported and fails validation; use the field. A timezone database from the Go standard library ships in the binaries as a fallback when the host has none. DST behavior for a schedule inside the skipped or repeated hour is not specified in the documentation.

    Source: Kubernetes Docs — CronJob

    Vercel Cron Jobs

    On Hobby, an expression that runs more often than daily fails the deployment outright, and the job may be invoked at any point inside the specified hour to spread load — 0 8 * * * can fire anywhere between 08:00:00 and 08:59:59. On paid plans invocation lands inside the specified minute.

    Source: Vercel Docs — cron jobs

    Netlify Scheduled Functions

    The dashboard displays the next run converted to your local timezone, which is a common source of confusion: the display is local, the schedule is UTC. Expressions can be declared inline (TypeScript and JavaScript only) or in netlify.toml.

    Source: Netlify Docs — scheduled functions

    Cloudflare Workers Cron Triggers

    The trap here is not DST, it is day-of-week numbering: Cloudflare counts 1 = Sunday through 7 = Saturday, where POSIX cron uses 0 = Sunday through 6 = Saturday. A crontab line copied straight from a Linux box can therefore run a day late. wrangler dev runs the local runtime with TZ=UTC to match production.

    Source: Cloudflare Docs — cron triggers

    AWS EventBridge Scheduler

    EventBridge Scheduler is the newer service and takes a timezone from the IANA database; classic EventBridge scheduled rules are documented separately and behave differently, so check which one you are using. All schedule types invoke with 60-second precision — a schedule set for 01:00 fires between 01:00:00 and 01:00:59 unless you configure a flexible time window, which widens that spread deliberately.

    Source: AWS Docs — EventBridge Scheduler schedule types

    Azure Functions timer trigger

    This is the cleanest DST story of any platform in this table: set WEBSITE_TIME_ZONE (a Windows zone id, or an IANA name such as America/New_York on Linux) and NCRONTAB expressions are interpreted in that zone with DST applied automatically — no expression edits at the transition. Note the field order: the leading field is seconds, so every position shifts by one compared with POSIX cron.

    Source: Microsoft Learn — timer trigger for Azure Functions

    pg_cron (incl. Supabase)

    Changing cron.timezone requires a PostgreSQL restart, which on a managed platform may not be something you control — on Supabase, treat the schedule as GMT and convert. The seconds syntax is standalone: '30 seconds' is valid, but you cannot add a seconds field to a five-field expression.

    Source: citusdata/pg_cron

    Apache Airflow

    This is the distinction that catches people: a timezone-aware DAG on a cron schedule respects DST, so 0 0 * * * in US/Eastern runs at 04:00 UTC in summer and 05:00 UTC in winter. The same DAG on a timedelta schedule respects DST for the start date only and then holds a fixed UTC time forever. Same DAG, same intent, two different behaviors depending on which schedule type you chose.

    Source: Airflow Docs — time zones

    Quartz Scheduler

    Quartz documents this explicitly and it is the clearest warning any scheduler gives: fire times scheduled between midnight and 02:00 can be skipped or repeated depending on which way the clock moves, so 01:05 may happen twice and 02:05 may never happen. Misfire instructions decide what happens afterwards — fire immediately, or skip to the next scheduled time. If the job is not idempotent, keep it out of that window entirely.

    Source: Quartz — CronTrigger API docs

    Spring @Scheduled

    Six fields with seconds first, so 0 * * * * MON-FRI means once a minute on weekdays, not once a second. The zone attribute takes anything TimeZone.getTimeZone(String) accepts and defaults to the scheduler's own timezone — which in a container is usually UTC, not the timezone of whoever wrote the expression.

    Source: Spring Framework — @Scheduled javadoc

    Celery Beat

    One operational trap is called out upstream: the database-backed scheduler does not reset when timezone-related settings change, so you have to update last_run_at by hand after changing the timezone. Skip that and tasks fire at times that match neither the old nor the new configuration.

    Source: Celery Docs — periodic tasks

    Jenkins (build periodically)

    The TZ= line can appear anywhere in the trigger and applies to every following line until another one overrides it; a bare TZ= resets to the controller's timezone. Prefer H over a literal 0H H * * * still runs each job daily but spreads a dozen jobs across the window instead of stacking them all at midnight.

    Source: Jenkins Docs — change time zone

    systemd timers

    systemd does not use cron syntax at all — OnCalendar= takes a calendar specification with optional timezone suffix, and Persistent=true catches up a run missed while the machine was off. DST behavior around ambiguous and non-existent wall-clock times has been the subject of several upstream bug reports rather than a documented guarantee, so verify on your own systemd version before scheduling anything important inside the transition window.

    Source: systemd.time(7)

    Render cron jobs

    At most one run of a given cron job is active at a time, which removes the overlap problem you would otherwise solve with a lock file. A run is stopped after 12 hours.

    Source: Render Docs — cron jobs

    Railway cron jobs

    Railway does not guarantee execution to the minute — runs can vary by a few minutes, and the minimum frequency is every five minutes. Do not build anything that depends on two jobs firing in a known order.

    Source: Railway Docs — cron jobs

    Fly.io Machines

    Scheduled Machines give you interval buckets, not expressions: hourly, daily, weekly, monthly, with no control over the minute. If you need a real crontab, run Supercronic inside the container, in which case the DST and timezone behavior is Supercronic's rather than Fly's.

    Source: Fly.io Docs — task scheduling

    How to schedule so this never matters

    Four rules, in order of how much trouble they save:

    • Keep jobs out of 00:30–03:30 local time. Nothing in this table can hurt you at 04:15. Most of it can hurt you at 02:30.
    • Schedule in UTC and convert for humans. UTC has no transitions, so a UTC schedule never runs twice and never gets skipped. The cost is that the local time drifts by an hour twice a year — acceptable for machine work, not for anything a person is waiting on.
    • Make the job idempotent. If running twice is harmless, the fall-back hour stops being a risk. This is worth more than any scheduling trick.
    • Set the timezone explicitly, even when the default is what you want. A default that is not written down is a default that changes when someone else edits the manifest.

    Frequently asked questions

    Does cron run in UTC or local time?

    Plain Unix cron uses the system's local timezone, not UTC. Managed platforms mostly go the other way: GitHub Actions, Vercel, Netlify, Cloudflare Workers, Render and Railway all evaluate schedules in UTC with no option to change it.

    What happens to a cron job at 2:30 AM when the clocks change?

    It depends on the scheduler. Unix cron runs a skipped fixed-time job immediately after the shift and avoids running a repeated one twice. Quartz documents the opposite risk — a time in the repeated hour can fire twice, and one in the skipped hour may never fire. Several platforms do not document the behavior at all.

    Which schedulers let me set a timezone?

    Unix cron (CRON_TZ), Kubernetes CronJob (.spec.timeZone), GitLab CI (cron_timezone), AWS EventBridge Scheduler, Azure Functions (WEBSITE_TIME_ZONE), pg_cron (cron.timezone), Airflow, Quartz, Spring @Scheduled, Celery, Jenkins and systemd timers. GitHub Actions, Vercel, Netlify, Cloudflare Workers, Render, Railway and Fly.io do not.

    Do UTC schedules avoid DST problems entirely?

    Yes for correctness — UTC has no transitions, so a job never runs twice or gets skipped. No for expectations: a 09:00 UTC job that people think of as a 09:00 local job will arrive an hour early or late for half the year.

    Why does the same cron expression behave differently on two platforms?

    Three reasons, usually in this order: the field count differs (Quartz, Spring and Azure put seconds first; EventBridge adds a year), day-of-week numbering differs (Cloudflare and EventBridge count 1 = Sunday, POSIX counts 0 = Sunday), and the evaluation timezone differs.

    Related cron expressions

    Copied