Skip to main content
ZEKILO Dev
Convert

Cron in GitHub Actions and Kubernetes: the UTC Pitfall

Updated 2026.10.03 · 5 min read

GitHub Actions runs cron schedules in UTC by default and Kubernetes uses the controller's time zone. Learn the 5-field syntax, time zone fields and the OR rule.

Open Cron Expression Parser

A cron expression carries no time zone; the platform that runs it decides. GitHub Actions evaluates schedule in UTC by default, and a Kubernetes CronJob without a time zone uses the local time zone of the kube-controller-manager. So 0 9 * * 1-5 means 09:00 UTC on GitHub, not 09:00 where you live. Either convert the time to UTC yourself or set the platform’s time zone field. To read an expression in plain words and list its next runs in UTC or your own zone, paste it into the Cron Expression Parser.

The five fields

Both platforms use the classic five-field form:

┌──────── minute         0-59
│ ┌────── hour           0-23
│ │ ┌──── day of month   1-31
│ │ │ ┌── month          1-12
│ │ │ │ ┌ day of week    0-6 (0 = Sunday)
* * * * *
Operator Meaning Example
* Every value * * * * * runs every minute
, List 0 9,18 * * * runs at 09:00 and 18:00
- Range 0 9 * * 1-5 runs at 09:00 Monday to Friday
/ Step */15 * * * * runs every 15 minutes

POSIX defines only the asterisk, single numbers, ranges and lists. Step values with / are an extension from Vixie cron, which both GitHub Actions and Kubernetes document as supported. Expressions with six fields (seconds first) or seven (Quartz) belong to other schedulers.

Some reference results, evaluated in the Asia/Seoul time zone:

Expression Starting from Next runs
0 9 * * 1-5 Friday 2026-10-02 10:00 Mon 10-05 09:00, then 10-06, 10-07, 10-08 and 10-09 at 09:00
*/15 * * * * 2026-10-02 10:07 10:15, 10:30, 10:45, 11:00, 11:15
0 0 31 2 * Any time Never, because February has no 31st
0 25 * * * Any time Invalid: the hour field must be 0 to 23

How the two platforms differ

As of October 2026, the official documentation describes the following:

Item GitHub Actions schedule Kubernetes CronJob
Default time zone UTC Local time zone of the kube-controller-manager
Setting a time zone timezone key with an IANA name .spec.timeZone with an IANA name (stable since v1.27)
Macros such as @daily Not supported @yearly, @monthly, @weekly, @daily, @hourly
Step values Supported Supported
Names JAN-DEC, SUN-SAT sun to sat for the day of week
Question mark ? Not listed Same meaning as *
Shortest interval Once every 5 minutes Not stated; the documentation’s example runs every minute

Kubernetes also notes that putting CRON_TZ= or TZ= inside .spec.schedule is not supported and fails validation. Use the time zone field.

The UTC pitfall in numbers

Seoul and Tokyo are both UTC+9, with no daylight saving time. Suppose you commit 0 9 * * 1-5 to a GitHub workflow at 10:00 on Friday, 2 October 2026, local time, expecting a 09:00 start on weekdays. Evaluated in UTC, the first run is that same Friday at 09:00 UTC, which is 18:00 in Seoul and Tokyo, and every later run is at 18:00 too.

To run at 09:00 local time you subtract nine hours:

Goal (UTC+9) Expression in UTC Note
09:00, Monday to Friday 0 0 * * 1-5 00:00 UTC is 09:00 the same day
08:00, Monday to Friday 0 23 * * 0-4 23:00 UTC is the previous day, so the weekdays shift too
08:00, Monday to Friday 0 23 * * 1-5 Wrong: this runs Tuesday to Saturday at 08:00

The second and third rows are the part people miss. When the conversion crosses midnight, the day-of-week and day-of-month fields have to move with it. Some schedules cannot be converted at all: “00:00 on the 1st, local time” is 15:00 UTC on the last day of the previous month, and five-field cron has no way to say “last day”.

Declaring the time zone avoids the arithmetic. Following the documented syntax:

# GitHub Actions
on:
  schedule:
    - cron: "0 9 * * 1-5"
      timezone: "Asia/Seoul"
# Kubernetes CronJob
spec:
  schedule: "0 9 * * 1-5"
  timeZone: "Asia/Seoul"

On Kubernetes this matters even if your cluster happens to behave today: with a managed control plane you may not know which time zone the kube-controller-manager uses, so set .spec.timeZone explicitly. For zones that do observe daylight saving time, a schedule written in UTC moves by an hour in local terms twice a year. GitHub documents that with timezone set, a schedule that falls in the skipped hour of a spring-forward transition advances to the next valid time; its example is a 2:30 AM schedule that advances to 3:00 AM.

Day of month and day of week: either one matches

POSIX specifies that when the day of month (or the month) and the day of week are both restricted, a day matching either one is matched. The two fields are combined with OR, not AND.

0 0 13 * 5 therefore does not mean “Friday the 13th”. Starting from Friday 2026-10-02 10:00 in Asia/Seoul, it runs at 00:00 on Friday 10-09, Tuesday 10-13 and Friday 10-16: every Friday and also the 13th.

GitHub’s documentation says to use POSIX cron syntax. The Kubernetes page links to the format of the Go library robfig/cron, whose implementation matches a day when either field matches as long as neither is an asterisk. If you need “the 13th, only when it is a Friday”, schedule one of the two conditions and test the other inside the job. The Cron Expression Parser points out this rule when both fields are set.

Other behavior worth knowing

GitHub Actions

  • Scheduled runs can be delayed when load is high, and the documentation names the start of every hour as a high-load time; if load is high enough, some queued jobs may be dropped. Choose a minute other than 0, such as 17 0 * * *.
  • Scheduled workflows run on the latest commit of the default branch, and the workflow file must exist on that branch.
  • In a public repository, scheduled workflows are disabled automatically when there has been no repository activity for 60 days.

Kubernetes

  • A CronJob creates a Job approximately once per scheduled time. The documentation warns that in some circumstances two Jobs, or none, may be created, so Jobs should be idempotent.
  • concurrencyPolicy decides what happens when the previous Job is still running: Allow (the default), Forbid or Replace.
  • startingDeadlineSeconds sets how late a missed Job may still start. If more than 100 schedules were missed, the controller does not start the Job and logs an error.

Checking a schedule with ZEKILO Dev

  1. Open the Cron Expression Parser and paste the expression. The format is detected from the number of fields.
  2. Set the time zone to UTC to see what GitHub Actions does by default, or to Asia/Seoul or Asia/Tokyo to see what you intended.
  3. Compare the description and the next five run times with what you expect.

The expression is parsed in your browser and is not sent to a server.

Summary

  • Cron has no time zone of its own. GitHub Actions defaults to UTC; Kubernetes defaults to the controller’s local zone.
  • Convert to UTC carefully, moving the day fields when the time crosses midnight, or set timezone (GitHub) or .spec.timeZone (Kubernetes).
  • When both day fields are restricted, a day matching either one runs the job.
  • Avoid minute 0 on GitHub Actions, and make Kubernetes Jobs idempotent.

Tools for this guide

Sources

More guides