2026-10-24
Will your cron job skip or double-run on November 1? Daylight saving and cron
US clocks fall back on Sunday, November 1, 2026, at 2:00 AM local. What that does to cron, CRON_TZ, systemd timers, and GitHub Actions, and how a heartbeat catches a skipped run.
On Sunday, November 1, 2026, most of the United States turns clocks back at 2:00 AM local daylight time. That instant becomes 1:00 AM standard time, so 1:00 through 1:59 happens twice. NIST states the 2026 window directly: daylight saving time runs from March 8 at 2:00 AM local to November 1 at 2:00 AM local. The US Naval Observatory lists the same dates. Hawaii, most of Arizona, and several territories do not change. Everywhere else, a wall-clock job in the repeated hour can run twice, and a job in a different hour the following spring can skip. Which one happens depends on the scheduler.
The two mornings, and they are not the same window
November 1 is the fall-back. The repeated times are 1:00 to 1:59. A daily job at 1:30 has two real instants that morning, an hour of UTC apart. A daily job at 3:30 has one.
March 8, 2026 is the spring-forward. At 2:00 AM local, clocks jump to 3:00 AM. The times from 2:00 through 2:59 do not exist. A job at 1:30 is fine, because 1:30 still happens once. A job at 2:30 is the one that can disappear. If you remember a single rule, remember the windows: fall-back trouble is 1:00–2:00, spring-forward trouble is 2:00–3:00. They are not the same hour.
What the cron manuals actually say
There is no single POSIX rule for this. The crontab(5) manual describes plain field matching, and it is blunt. A time that does not exist, the missing hour during the change, never matches, so the job does not run. A time that happens twice matches twice, so the job runs twice.
The cron(8) manual describes an extra adjustment in cronie and the BSD crons. Clock steps under three hours are special, and only for jobs at a specific time that run less often than once an hour. A jump forward runs a skipped job immediately. A jump backward tries not to run that job twice. Jobs that run more often stay on the match rule: an hourly job can fire in both copies of the repeated hour, and it can miss the skipped spring hour.
A daily 1:30 job on cronie is in the bucket cron(8) tries to run once. The same line on a cron that only implements the crontab(5) match can run twice. An hourly job is scheduled normally on cronie, so the extra 1:00 is a real extra run. If you do not know which binary is installed, stay out of both windows.
UTC, and CRON_TZ when you cannot use UTC
UTC does not have a repeated hour on November 1. A job at 06:15 UTC runs at 06:15 UTC that day and the next. The mapping to US wall time changes by an hour, which is what you want for backups, invoice exports, and anything that must happen once per calendar date in UTC. It is the wrong choice only when the requirement really is '1:30 AM in New York,' including after the clocks change.
CRON_TZ, from crontab(5), sets the zone for that file. The times you type are in that zone. Log lines stay in the daemon's zone, so a UTC host logs a New York job with a UTC timestamp. CRON_TZ does not disable daylight saving. America/New_York still repeats 1:00–2:00 on November 1 and still skips 2:00–3:00 on March 8. It only stops the schedule from following the server zone by accident.
# Once a day, no US daylight-saving edge.
CRON_TZ=UTC
15 6 * * * /usr/local/bin/nightly-report
# Wall-clock time, parked outside both transition windows.
CRON_TZ=America/New_York
30 3 * * * /usr/local/bin/nightly-report3:30 AM local is after the repeated hour and after the missing hour. 06:15 UTC never enters either. Prefer the UTC line unless a person is waiting on the local time.
systemd timers
systemd calendar events are wall-clock times. The systemd.time manual gives the syntax, including an explicit UTC or IANA zone, and does not say what a gap or a repeated hour does. A Debian report from the 2023 spring change describes a daily 02:00 timer that did not run, because 02:00 did not exist. A systemd issue describes OnCalendar timers producing no firings during the duplicated hour on the way back to standard time. Persistent=true replays a timer that elapsed while the machine was off. It is not a daylight-saving fix. AccuracySec= defaults to one minute, so a timer can also elapse slightly late on a normal day.
# /etc/systemd/system/nightly-report.timer
[Unit]
Description=Nightly report
[Timer]
OnCalendar=*-*-* 06:15:00 UTC
Persistent=true
AccuracySec=1min
[Install]
WantedBy=timers.targetCheck it on the host with systemd-analyze calendar --iterations=5 '*-*-* 06:15:00 UTC'. For local time, name the zone and stay at 03:30: OnCalendar=*-*-* 03:30:00 America/New_York.
GitHub Actions runs in UTC unless you opt into a zone
The workflow syntax docs say scheduled workflows use POSIX cron and run in UTC by default. 15 6 * * * is 06:15 UTC every day, including November 1. It will not double or skip because the US changed clocks. The New York reading of that instant shifts by an hour. A hand-converted '1:30 AM Eastern' cron is wrong for half the year. The run still happens once.
Since March 2026, GitHub accepts an IANA timezone beside the cron. The docs describe one daylight-saving case: during the spring-forward, a schedule in a skipped hour advances to the next valid time. Their example is 2:30 AM moving to 3:00 AM. They do not say what November's repeated hour does, so a US-zone schedule between 1:00 and 2:00 that morning has no documented guarantee of running once. Leave that hour, or drop timezone and stay in UTC. Schedules run from the latest commit on the default branch, and the shortest interval is five minutes.
on:
schedule:
# 06:15 UTC every day. No US daylight-saving edge.
- cron: '15 6 * * *'
jobs:
report:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: ./nightly-report
- name: Ping Nightshift
if: always()
env:
NIGHTSHIFT_PING_URL: ${{ secrets.NIGHTSHIFT_PING_URL }}
run: |
if [ "${{ job.status }}" = "success" ]; then
curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL"
else
curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL/fail"
fiA heartbeat catches the skip, not the double
Put the ping in the job, after the work, as in the unit and the workflow above. A shell wrapper is the same idea for cron and systemd:
#!/bin/sh
set -eu
if /usr/local/bin/nightly-report; then
curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL"
else
curl -fsS -m 10 -X POST "$NIGHTSHIFT_PING_URL/fail" || true
exit 1
fiIf the scheduler skips the job, the script never runs and the monitor goes down after grace. A double run sends two success pings, so the monitor stays up. Make the job idempotent, keyed by the UTC date, so the second run does not send the file or the charge twice. A heartbeat will not notice the duplicate.
On Pro and Team a hard max of 1 success ping per hour alerts on a second success inside the window. That fits a daily job, not an hourly one. A manual re-run trips it too. The baseline detector will not catch two daily runs. It ignores windows under a floor of 10 runs.
Point the monitor at the same clock as the job. Nightshift evaluates a 5-field cron in the timezone you pick. For a UTC job, pick UTC. A daily 1:30 in America/New_York resolves to one slot on November 1, the first 1:30, during daylight time, not the second. An hourly expression resolves both 1:00s. On March 8, a daily 2:30 in that zone resolves to 3:30 AM local that morning, then 2:30 the next day, and an hourly expression skips 2:00. GitHub's documented spring rule, 2:30 advancing to 3:00, is a different choice. Do not mix them. UTC on both sides removes the puzzle.
The NIST daylight saving page is the source for the 2026 dates above. Other countries change on other mornings, often a different hour. Read the zone, don't reuse the US windows. If you want the missing ping to email you, Nightshift Watchdog is that monitor. Free covers 3 of them. The docs cover grace, /fail, and the same pattern for GitHub Actions and Vercel cron.