Cron expression examples with field-by-field explanations

Quick answer

💡A standard Linux cron expression has five whitespace-separated fields: minute (0-59), hour (0-23), day-of-month (1-31), month (1-12), and day-of-week (0-7 where both 0 and 7 are Sunday). Use * for any value, */N for every N units, and comma-separated lists for specific values. Schedulers like Quartz and GitHub Actions use different field counts and different defaults — always check which variant your scheduler expects.

Error symptoms

  • bad minute in crontab — expression rejected at install time
  • Job never runs despite the expression looking correct
  • Job runs at the wrong time — UTC vs local time confusion
  • Expression works in crontab but fails in GitHub Actions or Kubernetes
  • Job skips one run per year around daylight saving time
  • failed to parse cron expression from a Java scheduler

Common causes

  • Six-field Quartz syntax used where Linux five-field is expected
  • Scheduler defaults to UTC while developer thinks in local time
  • Day-of-month and day-of-week both set — most schedulers treat this as OR
  • Step values or ranges copied from the wrong scheduler documentation
  • Daylight saving time causes a local hour to skip or repeat
  • Minimal PATH in cron environment causes the command to fail silently

When it happens

  • Setting up Linux crontab or systemd timers
  • Configuring Quartz or Spring scheduler in Java applications
  • Writing GitHub Actions scheduled workflows
  • Adding Kubernetes CronJobs or AWS EventBridge rules
  • Scheduling database backups or report generation jobs

The five-field cron syntax and what each field controls

The original Unix cron format, introduced by Vixie cron in the late 1980s and still the default in most Linux systems, uses five whitespace-separated fields followed by the command to execute. From left to right the fields are: minute (0-59), hour (0-23), day-of-month (1-31), month (1-12 or names like JAN, FEB), and day-of-week (0-7, where both 0 and 7 represent Sunday, or names like MON, TUE). Understanding what each field represents and what order they appear in is the single most important skill for writing correct cron expressions.

The order catches many developers because it is the inverse of what you might naturally say in English. You would say 'run this every day at nine in the morning Monday through Friday,' which suggests the time comes first and the day qualifier comes second. In cron syntax, minute comes before hour, and both come before the day fields: 0 9 * * 1-5. The minute field being leftmost is counter-intuitive for many new users, who sometimes write 9 0 * * 1-5 (the ninth minute of midnight each weekday) when they intended 9 AM.

The asterisk * in any field means 'every valid value for this field.' So * in the minute field means 'every minute,' and * in the month field means 'every month.' A common mistake is writing 0 * * * * and expecting it to mean 'every hour' when it actually means 'at minute 0 of every hour' — which is correct but requires understanding that minute is the first field, not hour. The expression * * * * * means 'every minute of every hour of every day,' which runs 1440 times per day.

The day-of-month and day-of-week fields interact in a non-obvious way. When both fields contain a value that is not *, most Vixie cron implementations apply OR logic: the job runs when the day-of-month matches OR when the day-of-week matches. This means 0 9 1 * 1 runs at 09:00 on the first of every month AND at 09:00 every Monday, not only at 09:00 on Mondays that happen to be the first of the month. If you need AND logic (first Monday of the month), you must implement it in the command itself — for example using a shell conditional that checks the date output. Quartz cron handles this differently by requiring one of the two fields to be ? (unspecified), making the intent explicit.

Environment variables set in the user's shell profile (.bashrc, .zshrc, .profile) are not loaded by cron. The cron daemon runs with a minimal environment that typically includes only HOME, LOGNAME, SHELL, and PATH=/usr/bin:/bin. Commands that work interactively but fail silently under cron almost always fail because of a missing PATH entry or an unset environment variable. Set SHELL, PATH, and any required environment variables at the top of your crontab file or use a wrapper script that sources the required environment before executing the main command.

Example 1

Five-field syntax with explicit PATH to avoid silent command failures.

Every weekday at 09:00 in Linux crontab

❌ Wrong

# Broken: six fields mistakenly used in Linux crontab
0 0 9 * * 1-5 /usr/local/bin/daily-report.sh
# Error: bad minute - Linux cron sees 6 fields and rejects
# or shifts field positions causing wrong schedule
SHELL=/bin/sh
PATH=/usr/bin:/bin

✅ Fixed

# Fixed: five fields for Linux crontab
0 9 * * 1-5 /usr/local/bin/daily-report.sh
# minute=0  hour=9  day-of-month=*  month=*  day-of-week=1-5 (Mon-Fri)
# Runs at 09:00 every Monday through Friday
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
# Test manually: /usr/local/bin/daily-report.sh
# Verify: crontab -l | crontab -

Linux crontab expects exactly five time fields. Adding a seconds field shifts all field positions and produces either a parse error or a job that runs at the wrong time. The fixed expression has five fields and explicitly sets SHELL and PATH — without PATH, commands that work interactively often fail silently under cron because the cron environment has a minimal path that does not include /usr/local/bin.

Step values, ranges, and lists in cron fields

Beyond simple single values and the all-values wildcard, cron fields support three types of compound expressions: ranges, lists, and step values. These can be combined to express most periodic schedules without any shell scripting.

A range is written as start-end and means every value from start to end inclusive. The hour field 9-17 means hours 9, 10, 11, 12, 13, 14, 15, 16, and 17 — from 9 AM to 5 PM. Ranges are most commonly used in the day-of-week field to express weekday ranges like 1-5 (Monday through Friday) or weekend ranges like 6-7 (Saturday and Sunday). Note that the day-of-week field uses 0-7 with both 0 and 7 representing Sunday and 1 representing Monday — not the ISO weekday numbering where Monday is 1 and Sunday is 7.

A list is written as value1,value2,value3 and means exactly those values with no implied continuity between them. The month field 1,4,7,10 means January, April, July, and October — the first month of each quarter. Lists and ranges can be mixed within a single field: 1-5,7 in the day-of-week field means Monday through Friday plus Sunday, skipping Saturday. There is no limit on the number of items in a list, but mixing too many values in one field often indicates the schedule logic belongs in the command rather than in the cron expression.

A step value is written as expression/N and means every Nth value within the expression. The most common form is */N (every N units): */15 in the minute field means minutes 0, 15, 30, and 45 — every 15 minutes. The step syntax */2 in the hour field means every even hour: 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22. Steps can also be applied to ranges: 8-17/2 in the hour field means every second hour between 8 AM and 5 PM: 8, 10, 12, 14, 16. This is useful for schedules like 'every two hours during business hours.'

A common mistake is confusing */30 in the minute field (every 30 minutes: minutes 0 and 30) with 30 in the minute field (only at minute 30 of each hour). Another mistake is using /1 as a step, which is equivalent to * — every value — but is sometimes written accidentally when the developer meant the value 1. The expression 0 8 * * 1 means 8 AM every Monday; the expression 0 8 * * */1 means 8 AM every day because */1 in the weekday field selects every weekday value from 0 to 7. Validate your expression using the ToolDock cron parser at /tools/cron-expression-parser to see the next five run times before deploying.

Special strings: @reboot, @daily, @hourly and their equivalents

Vixie cron and most compatible implementations support a set of non-standard shorthand strings prefixed with @ that replace the five-field time specification. These shortcuts are easier to read and communicate intent more clearly than the equivalent five-field expressions. However, they are not universally supported — Quartz, GitHub Actions, and some embedded schedulers do not recognize them.

@reboot runs the command once when the cron daemon starts, effectively at system boot. There is no five-field equivalent because five fields represent recurring time-based schedules, not one-time startup triggers. @reboot is useful for starting persistent processes that cron should restart after a server reboot: @reboot /usr/local/bin/start-worker.sh. Be aware that @reboot runs before network interfaces are fully configured in some distributions, which can cause commands that require network access to fail silently.

The time-based shortcuts map to specific five-field equivalents. @yearly (or @annually) maps to 0 0 1 1 * — midnight on January 1st. @monthly maps to 0 0 1 * * — midnight on the first of every month. @weekly maps to 0 0 * * 0 — midnight every Sunday. @daily (or @midnight) maps to 0 0 * * * — midnight every day. @hourly maps to 0 * * * * — the zeroth minute of every hour. The shortcuts are exactly equivalent to their five-field forms in terms of scheduling behavior.

The practical benefit of shortcuts is legibility. A crontab listing that uses @daily is immediately readable by anyone; one that uses 0 0 * * * requires the reader to mentally decode the field order. The limitation is that shortcuts only cover a narrow set of schedules — you cannot write @every-fifteen-minutes or @weekdays-at-nine. For any schedule that is not covered by the shortcuts, the five-field syntax is required.

Systemd timers, which are an alternative to cron on modern Linux systems (Ubuntu 20.04+, Debian 10+), use a completely different syntax called OnCalendar. The equivalent of @daily in systemd is OnCalendar=daily. Systemd's calendar syntax supports richer expressions like OnCalendar=Mon..Fri 09:00 (every weekday at 9 AM) and OnCalendar=*-*-* 00/6:00:00 (every six hours). Systemd timers have the significant advantage of handling missed runs: if the system was powered off at the scheduled time, systemd can execute the unit as soon as it boots using the Persistent=true option. Standard cron does not do this — missed runs are simply skipped with no notification.

Quartz cron is not the same as Linux cron

Quartz requires six or seven fields with a seconds field added at the start. It uses 1-7 for weekdays where 1 is Sunday (opposite of Vixie cron). Exactly one of day-of-month or day-of-week must be ? when the other has a value. Expressions copied from Linux crontab documentation will fail in Quartz and vice versa.

Example 2

Quartz requires six or seven fields including seconds and a ? placeholder.

Every six hours in Quartz scheduler

❌ Wrong

// Broken for Quartz: five-field Linux syntax
// Missing seconds field, missing ? placeholder
String expr = "0 */6 * * *";
CronExpression cron = new CronExpression(expr);
// Throws: org.quartz.CronExpression$ParseException:
// Unexpected end of expression.
// Expected 6 or 7 fields, only 5 were found

✅ Fixed

// Fixed for Quartz: six fields including seconds
// second  minute  hour  day-of-month  month  day-of-week
String expr = "0 0 */6 ? * *";
CronExpression cron = new CronExpression(expr);
// Runs at: 00:00, 06:00, 12:00, 18:00 UTC every day
// The ? in day-of-month means 'not specified'
// Required when day-of-week is also specified (or vice versa)
Scheduler scheduler = StdSchedulerFactory.getDefaultScheduler();
scheduler.scheduleJob(job, TriggerBuilder.newTrigger()
    .withSchedule(CronScheduleBuilder.cronSchedule(expr)).build());

Quartz requires six fields: second, minute, hour, day-of-month, month, day-of-week. When both day-of-month and day-of-week are set to non-wildcard values, Quartz throws an error. The ? character means 'not specified' and must appear in exactly one of those two fields. This syntax does not exist in Linux crontab, which is why expressions copied between the two environments break.

Cron in different environments: Vixie, Quartz, GitHub Actions, and AWS

Cron syntax is not universal. What looks like a valid cron expression in one environment may be rejected or silently misinterpreted in another. The most important differences are the number of fields, the handling of day-of-month versus day-of-week, and whether the scheduler adds fields for seconds or year.

Vixie cron (the standard on Debian, Ubuntu, RHEL, CentOS, and macOS) uses five fields: minute, hour, day-of-month, month, day-of-week. It supports ranges, lists, steps, and @ shortcuts. When both day-of-month and day-of-week are non-wildcard, Vixie cron applies OR logic. The cron daemon reads the crontab file directly; changes take effect within one minute without requiring a restart. The man 5 crontab page on your system documents the exact behavior for the installed version.

Quartz Scheduler — the standard Java cron library used by Spring, Quartz Enterprise, and many Java application servers — uses six or seven fields. The mandatory fields are: second (0-59), minute (0-59), hour (0-23), day-of-month (1-31 or ?), month (1-12 or names), day-of-week (1-7 where 1 is Sunday, or MON-SUN, or ?). The optional seventh field is year (1970-2099). Quartz does not allow both day-of-month and day-of-week to be non-wildcard simultaneously — exactly one must be the ? unspecified marker. Quartz also uses 1-7 for day-of-week where 1 is Sunday, which is different from Vixie cron where 0 and 7 are both Sunday and 1 is Monday.

GitHub Actions uses standard five-field cron syntax in the on.schedule trigger, with important constraints. All scheduled workflows run in UTC. The minimum interval is 5 minutes. During periods of high load, scheduled workflows may be delayed or skipped entirely. GitHub also stops executing scheduled workflows on a repository that has received no commits in 60 days — a surprising behavior that catches developers maintaining infrequently-updated projects. The @ shortcuts are not supported in GitHub Actions cron expressions.

AWS EventBridge (formerly CloudWatch Events) supports two expression types: rate expressions like rate(5 minutes) and cron expressions. AWS cron expressions use six fields: minute, hour, day-of-month, month, day-of-week, year. Like Quartz, exactly one of day-of-month or day-of-week must be a question mark. AWS uses 1-7 for the day-of-week field where 1 is Sunday. The year field accepts specific years (2024-2199) or *. All EventBridge rules run in UTC. Kubernetes CronJobs use five-field Vixie syntax, but if the controller is down for more than the startingDeadlineSeconds window, missed jobs may be skipped depending on the concurrencyPolicy setting.

Timezone handling and DST edge cases

By default, cron runs in the system timezone — whatever timezone the server where the cron daemon executes is configured to use. Developers who think in their local timezone and write an expression for '9 AM local time' end up scheduling 9 AM in the server's timezone, which may be UTC, UTC+9, or something else entirely. This mismatch is the most common cron configuration mistake in globally distributed systems.

The standard Vixie cron daemon reads the system timezone from /etc/localtime or the TZ environment variable. To override the timezone for a specific crontab, set CRON_TZ or TZ at the top of the crontab file: CRON_TZ=America/New_York. Not all cron implementations support CRON_TZ — FreeBSD's cron does not, for example — so check your system's documentation before relying on it. Alternatives include writing the UTC time directly in the expression (which requires converting your intended local time to UTC), or using systemd timers which have first-class timezone support via the TimeZone= directive in the timer unit.

Daylight saving time creates two specific failure modes for time-based schedules. The first is the skipped hour. When clocks spring forward, a one-hour gap appears in the local timeline — clocks jump from 1:59 AM directly to 3:00 AM. Any job scheduled for a time in that gap, such as 2:30 AM, will not run that day because the local time simply does not exist. This is particularly problematic for nightly maintenance jobs and database backup scripts scheduled in the early morning hours. In North America the spring-forward transition happens on the second Sunday of March; in Europe it happens on the last Sunday of March.

The second failure mode is the repeated hour. When clocks fall back, a one-hour window in local time repeats — clocks roll back from 1:59 AM to 1:00 AM. Any job scheduled during that repeated window may run twice on that day. For a job that sends email notifications, charges customers, or processes financial transactions, running twice is significantly worse than running once or not at all. UTC-based scheduling avoids both of these problems entirely because UTC never observes daylight saving time and has no skipped or repeated hours.

A practical recommendation for production systems is to schedule all recurring jobs in UTC and document the UTC time prominently alongside the local time equivalent and the relevant timezone. Write comments in your crontab like: 0 2 * * * # 2:00 UTC = 10:00 AM JST = 9:00 PM EST (Nov-Mar) = 10:00 PM EDT (Mar-Nov). This documentation helps on-call engineers reason about job timing during incidents. For GitHub Actions and AWS EventBridge, UTC is enforced and there is no override — document the UTC schedule and the corresponding local time in the YAML comment or in the EventBridge rule description.

Schedule production jobs in UTC to avoid DST surprises

Daylight saving time can cause a scheduled 2 AM job to skip entirely in spring or run twice in autumn. UTC does not observe DST. When writing cron schedules, always document both the UTC time and the local equivalent in a comment next to every crontab entry.

Testing and debugging cron expressions before deploying

Testing cron expressions before deploying them to production prevents scheduled job failures that may go unnoticed for hours or days. The most common mistake is assuming a visually plausible expression is correct — the only reliable verification is to evaluate the expression programmatically and inspect the next several scheduled run times.

The simplest local test for a Linux crontab entry is to use the crontab command itself. After installing a test entry, wait for the next scheduled minute and check the system log: journalctl -u cron on systemd-based systems, or grep cron /var/log/syslog on Debian-based systems, or /var/log/cron on RHEL-based systems. The cron daemon logs every job execution including the user, the command, and the process start. If the job starts but produces no output, the problem is in the command; if the job never appears in the log at all, the expression is wrong, the daemon is not running (check with systemctl status cron), or the command path is not executable.

For programmatic validation, the Python croniter library (pip install croniter) parses cron expressions and displays the next N scheduled times. The code is straightforward: from croniter import croniter; from datetime import datetime; cron = croniter('0 9 * * 1-5', datetime.now()); print([cron.get_next(datetime) for _ in range(5)]). This confirms not only that the expression parses correctly but also that the next run times fall on the expected days and hours. The cron-descriptor library (pip install cron-descriptor) converts a cron expression to an English description like 'At 09:00 AM, Monday through Friday,' which serves as a sanity check that the expression means what you think it does.

The ToolDock cron expression parser at /tools/cron-expression-parser evaluates any five-field or Quartz expression and lists the next ten scheduled times in both your local timezone and UTC. This is especially useful for catching timezone surprises — paste the expression, set your local timezone, and verify that the displayed times match your intention. The tool also shows the day-of-month and day-of-week interaction behavior.

For production monitoring, add a heartbeat check to every critical scheduled job. Tools like Healthchecks.io, Better Uptime, and Cronitor accept an HTTP ping at the start or end of each job run. If the expected ping does not arrive within the job's expected duration plus a configured tolerance, the monitoring service sends an alert. This detects not only jobs that fail but also jobs that silently stop running — a category of failure that expression testing alone cannot catch, and which is particularly common after server migrations, operating system upgrades, or crontab file overwrites by deployment scripts.

Cron expression checklist

  • Confirm whether your scheduler is five-field Vixie, six-field Quartz, or AWS EventBridge format.
  • Verify field order: minute, hour, day-of-month, month, day-of-week (Vixie) or add second at the start (Quartz).
  • In Quartz, set exactly one of day-of-month or day-of-week to ? — both specified is an error.
  • Check the timezone: does your scheduler run in UTC or local time? Document both in comments.
  • Validate with a parser: check the next five run times before deploying.
  • Set explicit SHELL and PATH at the top of your crontab to prevent silent command failures.
  • For jobs scheduled around 2 AM local time, verify DST behavior on spring-forward and fall-back days.
  • Add a heartbeat ping (Healthchecks.io or similar) to detect silently stopped jobs.

Frequently asked questions

What is the minimum cron interval?

Standard Linux cron has a one-minute minimum — the daemon wakes up once per minute and checks all scheduled jobs. For sub-minute scheduling, use systemd timers with OnCalendar=*:*:0/30 (every 30 seconds), or implement a loop inside the cron job. GitHub Actions enforces a five-minute minimum for scheduled workflows.

What does ? mean in a cron expression?

The ? character means 'not specified' and only exists in Quartz cron and AWS EventBridge syntax — not in Linux Vixie cron. It must appear in exactly one of the day-of-month or day-of-week fields when the other field has a non-wildcard value. This resolves the ambiguity of which day condition triggers the job.

Why does my cron job not run despite the correct expression?

The most common causes are: the command path is not in cron's minimal PATH, required environment variables are not set, the script lacks execute permission (chmod +x), or cron output is being silently discarded. Add 2>> /tmp/cron.log to your command to capture stderr, then check the system cron log with journalctl -u cron or grep cron /var/log/syslog.

How do day-of-month and day-of-week interact when both are set?

In Vixie/Linux cron, when both fields are non-wildcard, the job runs when EITHER condition is true (OR logic). So 0 9 1 * 1 runs at 09:00 on every Monday AND at 09:00 on the first of every month. For AND logic (first Monday of the month only), use a shell date conditional inside the command. Quartz avoids this by requiring one field to be ?.

Is cron timezone-aware?

Standard cron runs in the system timezone. Set CRON_TZ=America/New_York at the top of your crontab to override the timezone for that file, if your cron implementation supports it. GitHub Actions and AWS EventBridge always run in UTC with no override option. Systemd timers support explicit TimeZone= in the unit file.

What happens to a cron job scheduled during the DST spring-forward hour?

When clocks spring forward, local time jumps from 1:59 AM to 3:00 AM, skipping the 2 AM hour. A job scheduled for 2:30 AM local time will not run that day — the time does not exist. The next run will be the following day. For production jobs, avoid scheduling in the 1-3 AM window in DST-observing timezones, or schedule in UTC where DST does not apply.

What is the difference between 0 * * * * and * * * * *?

0 * * * * runs once per hour at minute 0 — 24 times per day. * * * * * runs every minute — 1440 times per day. The first field is always minutes, not hours. This is the most commonly confused field order. If you want hourly runs, the minute field should be a specific number like 0, not a wildcard.

How do I run a job every 15 minutes between 9 AM and 5 PM on weekdays?

Use */15 9-17 * * 1-5 which runs at minutes 0, 15, 30, and 45 of hours 9 through 17, Monday through Friday. Note this includes 17:00, 17:15, 17:30, and 17:45. To stop before 5 PM, use hours 9-16. Verify the next run times with a cron parser tool before deploying.

Why does the same expression work locally but fail in GitHub Actions?

GitHub Actions runs all scheduled workflows in UTC, while local development may use your local timezone. Also, GitHub Actions has a five-minute minimum interval and may delay runs during high load. Additionally, GitHub stops running scheduled workflows on repositories with no commits in 60 days — a common surprise for low-activity projects.

Related guides

All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-07.