Cron Schedules Across Platforms: Crontab, GitHub, Kubernetes, AWS

Building a cron schedule that does what you mean: the five fields, the day-of-month OR rule, time zones and daylight saving, and how GitHub, Kubernetes, Quartz and EventBridge read it.

· 11 min read

Illustration for the Cron Expression Generator guide

A cron expression is five short fields, and it is still easy to write one that runs on the wrong days, at the wrong hour or not at all. The syntax itself is covered in our crontab syntax guide; this article is about turning an intention ("every weekday at half past nine, London time") into a schedule that does exactly that on the platform you deploy to. It covers the rule that makes day fields behave unexpectedly, time zones and daylight saving, and how crontab, GitHub Actions, Kubernetes, Quartz and AWS EventBridge read the same schedule differently. The Cron Expression Generator builds the expression from plain choices and shows the next run times and the platform versions side by side.

The five fields, briefly

┌ minute (0-59)
│ ┌ hour (0-23)
│ │ ┌ day of month (1-31)
│ │ │ ┌ month (1-12 or JAN-DEC)
│ │ │ │ ┌ day of week (0-6 or SUN-SAT, 7 is also Sunday)
30 9 * * 1-5

Each field takes *, a number, a list (1,15), a range (1-5) or a step (*/10, 0-30/10). Steps start at the field's minimum, so */10 in the minute field means 0, 10, 20, 30, 40, 50, and in the day-of-month field */10 means 1, 11, 21, 31. A step with a start but no range, such as 5/15, is accepted by some parsers but not by classic Vixie cron, which most Linux systems run; write 5-59/15 to be safe. That is the form the generator uses.

Start from the sentence, not the syntax

The most reliable way to write a schedule is to say it in words first, then fill the fields one at a time, then read it back. The generator does the reading back for you with cronstrue, which turns 30 9 * * 1-5 into "At 09:30, Monday through Friday". A few translations people often need:

IntentionExpression
Every 5 minutes*/5 * * * *
Every hour at :1717 * * * *
Every 6 hours0 */6 * * * (00:00, 06:00, 12:00, 18:00)
Weekdays at 09:3030 9 * * 1-5
Twice a day, 08:00 and 20:000 8,20 * * *
Every 15 minutes in business hours*/15 9-17 * * 1-5 (last run 17:45)
1st and 15th at 06:000 6 1,15 * *
Quarterly, first day at 06:000 6 1 1,4,7,10 *
Sundays at 03:3030 3 * * 0

Notice the business-hours row: 9-17 in the hour field includes 17, so the job also runs at 17:00, 17:15, 17:30 and 17:45. If the window ends at 17:00, use 9-16 and add a separate 0 17 * * 1-5 line if you need the final run.

The day-of-month OR rule

This one surprises nearly everyone once. When both the day-of-month field and the day-of-week field are restricted (neither is *), cron runs the job when either matches. The crontab(5) manual states it directly, and POSIX specifies the same behaviour.

Calendar for November 2026 showing which days the expression 0 9 1 * 1 fires: the 1st (a Sunday, matched by day of month) plus every Monday, the 2nd, 9th, 16th, 23rd and 30th, because cron joins the two day fields with OR rather than AND
Calendar for November 2026 showing which days the expression 0 9 1 * 1 fires: the 1st (a Sunday, matched by day of month) plus every Monday, the 2nd, 9th, 16th, 23rd and 30th, because cron joins the two day fields with OR rather than AND

So 0 9 1 * 1 does not mean "the first of the month if it is a Monday". It means "the first of every month, and also every Monday". There is no five-field expression for "first Monday of the month". The usual workaround is to run on Mondays that fall on days 1 to 7 by putting the date test in the command:

0 9 * * 1  [ "$(date +\%d)" -le 07 ] && /opt/jobs/monthly-report

The % must be escaped as \% inside a crontab, because cron turns an unescaped % into a newline. Quartz and EventBridge offer 1#1 (first Monday) instead, which is one reason to schedule such jobs there.

Two related traps: day 31 simply does not occur in April, June, September or November, so 0 0 31 * * silently skips five months a year, and 0 0 30 2 * never runs at all. The generator warns about both. For "last day of the month" in five fields, run on days 28 to 31 and test whether tomorrow is the 1st.

Time zones and daylight saving

Classic cron runs in the daemon's local time zone, which on servers is often UTC but on a laptop or a VM built from a desktop image may be local time. In a zone with daylight saving, local times between roughly 01:00 and 03:00 can be skipped or repeated once a year. Implementations differ in how they compensate: some run a skipped job once at the change, others do not. The safe habits are to run servers in UTC, or to keep daily jobs away from the change window.

When you preview a schedule in the generator, the next run times are computed by cron-parser in the zone you choose, with daylight saving applied. A job at 0 9 * * 1-5 in America/New_York runs at 13:00 UTC until the first Sunday of November and at 14:00 UTC after it. If you copy that expression to a UTC-only platform, it will run at the same UTC time all year, which means an hour off local time for half of it.

Some cron implementations, including cronie, accept a CRON_TZ=Europe/London line in the crontab to set the zone for the entries that follow. Check your system's crontab(5) page before relying on it.

Platform by platform

The same intention is written differently on each platform.

PlatformFieldsTime zoneNotable rules
crontab (Vixie, cronie)5daemon's local zone, CRON_TZ on someOR rule for day fields; % must be escaped
Kubernetes CronJob5controller's zone unless spec.timeZone is settimeZone stable since 1.27; concurrencyPolicy controls overlap
GitHub Actions5always UTCminimum interval 5 minutes; runs can be delayed under load
Quartz / Spring6 or 7 (seconds first, optional year)JVM or trigger zoneone of the day fields must be ? in Quartz; 1 = Sunday
AWS EventBridge6 (year last), wrapped in cron()UTC for rules; Scheduler lets you pickone of the day fields must be ?; 1 = Sunday; supports L, W, #

Here is "weekdays at 09:30" on each:

crontab / Kubernetes / GitHub   30 9 * * 1-5
Quartz                          0 30 9 ? * MON-FRI
AWS EventBridge                 cron(30 9 ? * MON-FRI *)

The day-of-week numbering is the classic off-by-one. Unix counts Sunday as 0; Quartz and EventBridge count Sunday as 1, so Unix 1-5 (Monday to Friday) would mean Sunday to Thursday if pasted unchanged. Using names such as MON-FRI avoids the problem, and the generator writes names for those two platforms. Because Quartz and EventBridge require ? in one day field, they cannot express the Unix OR behaviour at all; if both day fields are restricted, the generator tells you to create two schedules.

On GitHub Actions, scheduled workflows run on the default branch, are evaluated in UTC, and GitHub notes that runs can be delayed during periods of high load, especially at the start of every hour. An odd minute such as 17 tends to start closer to its time than 0. Very frequent schedules are throttled to once every five minutes.

On Kubernetes, also set concurrencyPolicy: Forbid if two runs must never overlap, and startingDeadlineSeconds so a CronJob that missed its time because the controller was down does not run late at an unexpected moment.

Worked examples

  1. Nightly backup at 02:30, server in Europe/Berlin. 30 2 * * * falls inside the daylight saving change window on the last Sunday of March, when 02:00 to 03:00 does not exist locally. Moving it to 30 3 * * *, or running the server in UTC, removes the ambiguity.
  2. A GitHub Actions report at 08:00 New York time. In winter that is 13:00 UTC, in summer 12:00 UTC. Pick one (0 13 * * 1-5) and accept an hour's shift, or run at both times and have the job exit when it is not 08:00 locally.
  3. Billing on the 1st and 15th. 0 6 1,15 * * is safe in every month. The preview shows both dates for the next few months.
  4. Converting a Linux job to EventBridge. 0 */4 * * * becomes cron(0 0/4 * * ? *): steps are written start/step, the day-of-week field takes ? and a year field is added.

A checklist before you deploy a schedule

  • Read the English description and the next five runs; do they match the sentence you started from?
  • Is either day field *? If not, do you really want OR?
  • Which time zone will the platform use, and does daylight saving shift it?
  • For Quartz or EventBridge, did Sunday stay Sunday?
  • Can two runs overlap? Use flock -n in crontab or concurrencyPolicy: Forbid in Kubernetes.
  • Does the command use absolute paths and log its output somewhere you will see it?

To check an expression someone else wrote, the Cron Expression Validator explains it and lists its next runs, and the Time Zone Converter helps when you are translating local times to UTC.

Sources

  • crontab(5) manual page (cronie and Vixie cron), including the day-of-month and day-of-week rule and % handling.
  • The Open Group, POSIX.1-2017, crontab utility.
  • GitHub Docs, "Events that trigger workflows: schedule".
  • Kubernetes documentation, "CronJob" (timeZone, concurrencyPolicy, startingDeadlineSeconds).
  • Quartz Scheduler documentation, CronTrigger tutorial; Spring Framework CronExpression reference.
  • AWS documentation, "Cron expressions reference" for EventBridge Scheduler and EventBridge rules.
  • cron-parser and cronstrue project documentation.

Related searches

  • cron expression generator
  • cron day of month and day of week
  • github actions cron utc
  • kubernetes cronjob timezone
  • aws eventbridge cron format
  • quartz cron question mark
  • cron daylight saving time
  • cron last day of month
  • cron every weekday
  • convert crontab to eventbridge

Open the Cron Expression Generator