Skip to content
Back to Blog
Tutorials

Crontab Cheat Sheet: 50+ Cron Expressions and Modern Scheduler Guide

Crontab cheat sheet: 50+ copy-paste cron expressions, field-by-field syntax, the OR-trap, timezone fixes, plus Kubernetes/GitHub/AWS comparisons.

13 min read

Crontab Cheat Sheet: 50+ Cron Expressions, Syntax, and Modern Scheduler Guide

A cron expression is five fields (minute, hour, day-of-month, month, day-of-week) followed by a command. That grammar has run Unix scheduling since 1979 and now drives Kubernetes CronJobs, GitHub Actions, AWS EventBridge, and Vercel cron triggers. Once you know it, you can schedule on any of them.

This page is for developers who need an expression right now: a Linux task, a Kubernetes CronJob, a GitHub Actions trigger, or debugging why a five-minute job only fires hourly. Scroll to the quick reference table for copy-paste expressions, jump to the syntax section for the field rules, or open the Crontab Generator — a privacy-first crontab guru alternative that runs in your browser — to validate expressions live.

Cron expression quick reference table

Thirty expressions that cover roughly 90% of real scheduling needs. Each is valid POSIX five-field cron, so you can paste them into crontab -e, a Kubernetes schedule:, or a GitHub Actions cron:.

ScheduleCron ExpressionPlain English
Every minute* * * * *every minute, all day, every day
Every 5 minutes*/5 * * * *minute 0, 5, 10, …, 55
Every 15 minutes*/15 * * * *minute 0, 15, 30, 45
Every 30 minutes*/30 * * * *minute 0 and 30
Every hour0 * * * *top of every hour
Every 2 hours0 */2 * * *hour 0, 2, 4, …, 22
Every 6 hours0 */6 * * *hour 0, 6, 12, 18
Twice a day (9 AM + 9 PM)0 9,21 * * *minute 0 of hours 9 and 21
Every weekday at 9 AM0 9 * * 1-5Mon-Fri 09:00
Every weekend at 9 AM0 9 * * 0,6Sat and Sun 09:00
Daily at midnight0 0 * * *every day 00:00
Daily at 2:30 AM30 2 * * *low-traffic batch window
Every Monday at 9 AM0 9 * * 1Mondays 09:00
Every Friday at 5 PM0 17 * * 5Fridays 17:00
Every Sunday at midnight0 0 * * 0equivalent to @weekly
First of month at midnight0 0 1 * *1st day 00:00 — equivalent to @monthly
15th of every month at noon0 12 15 * *mid-month payroll window
Last day check (wrapper)0 0 28-31 * * + scriptrequires a date check
Quarterly (Jan/Apr/Jul/Oct 1st)0 0 1 JAN,APR,JUL,OCT *first day of each quarter
Annually (Jan 1)0 0 1 1 * or @yearlynew year midnight
Every 5 min on weekdays 9-5*/5 9-17 * * 1-5business-hours polling
Every 30 min on weekends*/30 * * * 0,6Sat/Sun monitoring
Twice an hour, 15 and 4515,45 * * * *offset from the :00 stampede
First Monday (wrapper)0 9 1-7 * 1 + AND checkwrapper needed (see below)
Macros@hourly @daily @weekly @monthly @yearlynon-standard but widely supported
On reboot only@rebootnon-standard, vixie cron only

Paste any of these into the Crontab Generator to preview the next five firings before you deploy.

Cron syntax decoded: the 5 fields

A cron expression is five whitespace-separated fields plus a command. Each field controls one slice of the schedule, and the same shape is used by every scheduler in this guide.

┌──────────── 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; 0 and 7 both mean Sunday)
│ │ │ │ │
* * * * *  command-to-run

Mnemonic: “My Hat Doesn’t Match Wendy’s” for Minute, Hour, Day-of-month, Month, Weekday. Left to right, smallest unit to largest.

Field-by-field allowed values

FieldRangeAliasesNotes
Minute0-59none0 means “on the hour”
Hour0-23none24-hour clock; 0 is midnight, 12 is noon
Day-of-month1-31noneinvalid days for a month silently never fire (Feb 31)
Month1-12JAN, FEB, MAR, …, DECcase-insensitive
Day-of-week0-7SUN, MON, TUE, …, SATboth 0 and 7 mean Sunday

Operators in detail

Five operators cover every standard cron expression:

OperatorMeaningExampleExpands to
*any value* * * * *every minute
,list0 9,12,17 * * *09:00, 12:00, 17:00
-inclusive range0 9-17 * * *every hour 09:00 to 17:00
/step*/15 * * * *minute 0, 15, 30, 45
mixedcombined0 9-12,14-17 * * *morning + afternoon, skip lunch

The step operator trips people up. */N is anchored to the field’s lowest value, not to the current time. */15 means “minute 0, 15, 30, 45 of every hour”, not “every 15 minutes from now”. Save at 12:03 and the next run is 12:15. With a non-wildcard base, 5/15 reads “start at 5, then every 15”: minute 5, 20, 35, 50.

Named months and weekdays

Write months and weekdays as names, case-insensitive:

0 0 1 JAN,APR,JUL,OCT *    # first of each quarter
0 9 * * MON-FRI            # weekdays at 9 AM
0 17 * * FRI               # Friday at 5 PM

Names are more readable in code review; numeric forms are slightly more portable. Pick one style per project.

Non-standard macros: @reboot, @daily, and friends

Most cron implementations accept these six shortcut macros:

MacroExpands toMeaning
@yearly / @annually0 0 1 1 *once a year, Jan 1 at midnight
@monthly0 0 1 * *first of each month at midnight
@weekly0 0 * * 0every Sunday at midnight
@daily / @midnight0 0 * * *every day at midnight
@hourly0 * * * *top of every hour
@reboot(special)once when the cron daemon starts

These macros are non-standard. Vixie cron and cronie support them, but Kubernetes CronJob, GitHub Actions, and AWS EventBridge do not. If you need portability, write the five-field form instead. @reboot rarely works in containers, because the cron daemon is usually not the init process.

50+ copy-paste cron expressions, grouped by use case

The quick reference table covers the common cases. The six groups below add more cron job examples for less common needs.

Every N minutes

* * * * *          # every minute
*/2 * * * *        # every 2 minutes
*/5 * * * *        # every 5 minutes — the cron expression every 5 minutes case
*/10 * * * *       # every 10 minutes
*/15 * * * *       # every 15 minutes
*/30 * * * *       # every 30 minutes
0,30 * * * *       # explicit minutes 0 and 30 (same as */30)
*/45 * * * *       # WARNING: fires at 0 and 45 only, then wraps

*/45 is a common foot-gun. Minute is 0-59, so the schedule fires at 0 and 45 and then wraps at the next hour. If you actually want a 45-minute cadence, you need an external worker.

Hourly variants

0 * * * *          # every hour at :00
30 * * * *         # every hour at :30
0 */2 * * *        # every 2 hours, even hour
0 */6 * * *        # every 6 hours
0 */12 * * *       # twice a day at 00:00 and 12:00
15 */2 * * *       # every 2 hours, offset by 15 min (avoids :00 spike)

Daily at specific times

0 0 * * *          # midnight (= @daily / @midnight)
30 2 * * *         # 02:30 — low-traffic batch window
0 9 * * *          # 09:00
45 23 * * *        # 23:45 — end-of-day rollups
0 9,12,17 * * *    # three times daily
0 9-17 * * *       # every hour from 09:00 through 17:00

Weekly schedules

0 9 * * 1-5        # weekdays at 9 AM
0 9 * * 0,6        # weekends at 9 AM
0 18 * * 5         # Fridays at 6 PM
0 0 * * 0          # Sunday at midnight (= @weekly)
0 9 * * MON,WED,FRI # Mon/Wed/Fri at 9 AM
*/30 9-17 * * 1-5  # every 30 min, business hours, weekdays

Monthly and quarterly

0 0 1 * *          # 1st of month at midnight (= @monthly)
0 0 15 * *         # 15th — payroll window
0 0 1,15 * *       # 1st and 15th — semi-monthly
0 0 1 */3 *        # quarterly: first of Jan, Apr, Jul, Oct
0 0 1 JAN,APR,JUL,OCT *  # same, named months
0 0 28-31 * *      # last few days — pair with a date-check wrapper

There is no native POSIX expression for “last day of month”. Either run a wrapper that checks date -d tomorrow +%d = 01, or switch to a scheduler that supports it natively (Quartz has L; Kubernetes does not).

Yearly and macro shortcuts

0 0 1 1 *          # Jan 1 at midnight (= @yearly / @annually)
0 0 25 12 *        # Christmas at midnight
@yearly            # = 0 0 1 1 *
@monthly           # = 0 0 1 * *
@weekly            # = 0 0 * * 0
@daily             # = 0 0 * * *
@hourly            # = 0 * * * *
@reboot            # special: once on daemon start (vixie cron only)

Any of these can paste into the Crontab Generator for a next-five-firings preview. It is the cheapest smoke test you can run before deploying.

Cron vs systemd timers vs cloud schedulers: a decision matrix

Cron is the default, but it is not always the best fit. The table below compares the seven schedulers you are most likely to choose between when you decide between cron and a systemd timer, between Kubernetes CronJob and a Vercel cron job, or when you migrate from crontab to a managed cloud scheduler.

Featurevixie cronsystemd timerK8s CronJobGHA scheduleAWS EventBridgeVercel CronCloudflare Workers
Field syntax5-field POSIXOnCalendar spec5-field POSIX + timeZone5-field POSIX6-field Quartz with ?5-field POSIX5-field POSIX
Minimum interval1 minute1 second1 minutebest-effort, ≥15 min recommended1 minute1 minute (Pro plan)1 minute
Explicit timezoneCRON_TZ=Persistent=truespec.timeZone (1.27+)UTC onlyScheduleExpressionTimezoneUTC onlyUTC only
Missed-run recoveryno (use anacron)yes (Persistent=true)yes (startingDeadlineSeconds)noyesnono
Retry / backoffnopartialyes (backoffLimit)retry on failureyesnoyes
Concurrency controlno (use flock)partialyes (concurrencyPolicy)nononono
@reboot supportyesyes (via OnBootSec=)nonononono

systemd timers: when to prefer them over cron

On systemd-based Linux, timers are a serious alternative. The calendar syntax is more readable, output flows into the journal, and missed runs can be replayed. A timer with a matching service looks like this:

# daily-report.timer
[Unit]
Description=Run daily report at 9 AM
[Timer]
OnCalendar=*-*-* 09:00:00
Persistent=true
Unit=daily-report.service
[Install]
WantedBy=timers.target
# daily-report.service
[Unit]
Description=Daily report job
[Service]
Type=oneshot
ExecStart=/usr/local/bin/daily-report.sh
User=reporter

Enable with systemctl enable --now daily-report.timer. The one feature that often justifies the switch is Persistent=true: if the machine was off at 9 AM, the timer fires as soon as it boots. Vixie cron has no equivalent without anacron. For service hardening, see our security best practices.

Kubernetes CronJob

Kubernetes wraps the POSIX schedule with extra primitives for concurrency, history retention, and an explicit timezone:

apiVersion: batch/v1
kind: CronJob
metadata:
  name: nightly-report
spec:
  schedule: "0 2 * * *"
  timeZone: "America/New_York"     # Kubernetes 1.27+
  concurrencyPolicy: Forbid        # never run two at once
  startingDeadlineSeconds: 300     # skip if delayed >5 min
  jobTemplate:
    spec:
      backoffLimit: 2
      template:
        spec:
          restartPolicy: OnFailure
          containers:
            - name: reporter
              image: reporter:1.4.0
              command: ["/usr/local/bin/report.sh"]

concurrencyPolicy: Forbid is the Kubernetes equivalent of flock. Without it, a slow run stacks onto its successor. See the field reference section for the remaining knobs.

GitHub Actions schedule caveats

GitHub Actions accepts standard five-field POSIX cron:

on:
  schedule:
    - cron: '0 9 * * 1-5'   # weekdays at 9 AM UTC

Scheduling is best-effort. When GitHub’s runners are under load, jobs can fire minutes late or skip a run entirely, so avoid intervals shorter than fifteen minutes. There is no timezone setting; schedules always run in UTC.

AWS EventBridge: Quartz-style six fields

AWS EventBridge uses a Quartz-flavored cron with six fields and a required ? in either day slot:

cron(0 9 * * ? *)

Field order: Minutes Hours Day-of-month Month Day-of-week Year. Either day field must be ? when the other is restricted, which is Quartz’s way of resolving the POSIX OR ambiguity. A direct copy from Linux crontab fails validation.

Vercel Cron, Cloudflare Workers, Render Cron Jobs

Newer serverless platforms standardize on five-field POSIX. A Vercel cron job lives in vercel.json as { "crons": [{ "path": "/api/cron/nightly", "schedule": "0 2 * * *" }] }. Cloudflare Workers Cron Triggers go in wrangler.toml:

[triggers]
crons = ["*/15 * * * *", "0 9 * * 1-5"]

Render uses render.yaml. All three run in UTC with no per-schedule timezone override, so design your schedules in UTC from the start.

7 cron debugging traps and how to catch them

Most “my cron job is not running” reports trace back to one of seven root causes. Walk the list before you blame the scheduler.

Trap 1: PATH is minimal

Cron starts jobs with a minimal $PATH, usually /usr/bin:/bin. Your interactive shell has /usr/local/bin, ~/.cargo/bin, and whatever else .bashrc adds. None of that exists in cron. It is the most common cron debugging path environment issue we see.

Symptom: node: command not found. Fix: set PATH at the top of the crontab, or use absolute paths in the command.

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin

*/15 * * * *  /usr/local/bin/poll-api.sh
0 9 * * *     /home/deploy/.cargo/bin/my-rust-cli

Trap 2: stdout and stderr are lost silently

By default cron sends output to a mail spool that nobody reads, so failures stay invisible. Redirect both streams:

*/15 * * * *  /usr/local/bin/job.sh >> /var/log/job.log 2>&1

For JSON output, pipe through jq. For log-line extraction, see our regex cheat sheet. On systemd timers, journalctl -u your-timer.service captures output without redirection.

Trap 3: Timezone drift between dev and prod

You wrote 0 9 * * * on your laptop in New York expecting 9 AM Eastern. The server runs UTC, so the cron fires at 9 AM UTC, which is 4 AM Eastern, before anyone is awake. Fix: set servers to UTC and write schedules in UTC, or pin the timezone explicitly.

CRON_TZ=America/New_York
0 9 * * *  /usr/local/bin/morning-report.sh

CRON_TZ works in vixie cron 3.0 and later, Kubernetes 1.27 and later has spec.timeZone, AWS EventBridge has ScheduleExpressionTimezone, and GitHub Actions is always UTC. For UTC, DST, and epoch math, see our Unix timestamp guide.

Trap 4: Unescaped % in commands

Cron treats unescaped % as a newline and feeds everything after it to the command as stdin, so date +"%Y-%m-%d" breaks. Escape every % as \%, or move the logic into a script:

0 0 * * *  echo "Run at $(date +"\%Y-\%m-\%d")" >> /tmp/log

Trap 5: Overlapping runs

A */5 * * * * job that occasionally takes seven minutes will start its next instance before the previous one finishes. Two copies then fight over the same database row, lock file, or API quota. Serialize with flock:

*/5 * * * *  flock -n /tmp/job.lock /usr/local/bin/job.sh

-n exits immediately when the lock is held. On Kubernetes, set concurrencyPolicy: Forbid instead. Lock file permissions matter; see security best practices.

Trap 6: @reboot in containers

@reboot runs once when the cron daemon starts. In a VM that maps to boot. In a container the cron daemon is usually not PID 1 and may not run at all. Do not use @reboot in containers; put run-once-at-boot logic in your entrypoint or in an init container.

Trap 7: POSIX day-of-month / day-of-week OR semantics

This is the most expensive cron trap we see. The POSIX rule is that when both day-of-month and day-of-week are restricted (neither is *), the schedule fires when either matches.

0 0 1 * 5 looks like “midnight on the 1st, only on Fridays”, but it fires on the 1st AND on every Friday: six to ten extra firings per month.

# WRONG: looks like "1st of the month, only if Friday"
0 0 1 * 5
# RIGHT: pick one constraint
0 0 1 * *          # every 1st of the month
0 0 * * 5          # every Friday
# AND semantics need a wrapper
0 0 1-7 * 5  [ "$(date +\%u)" = "5" ] && /script    # first Friday only

Paste suspect expressions into the Crontab Generator. The next-run preview makes the OR trap obvious.

Modern schedulers: when NOT to use cron

Cron is right for “run this command at roughly this time, on a fixed cadence”. It is the wrong tool for several adjacent problems:

  • Workflows with dependencies (run A, then B if A succeeded) belong in Airflow, Prefect, or Dagster.
  • Retry, exponential backoff, and dead-letter queues belong in Temporal, AWS Step Functions, or Sidekiq.
  • Sub-minute intervals are better served by a long-lived worker that sleeps between iterations.
  • Second-precision timing needs a dedicated daemon; managed schedulers explicitly disclaim exact timing.
  • Event-driven work belongs on webhooks, message queues, or change-data-capture streams.

Cron does not disappear in those systems. Airflow, Step Functions, and Sidekiq all accept cron expressions to schedule the entry point of their workflows, so the five-field grammar stays useful.

Kubernetes CronJob field reference

The decision matrix above showed a minimal CronJob. The full kubernetes cronjob syntax field reference is below.

FieldDefaultWhat it does
schedulerequiredPOSIX 5-field cron expression
timeZonecontroller TZexplicit timezone (1.27+); use IANA names
concurrencyPolicyAllowForbid skips new runs while previous active; Replace cancels it
startingDeadlineSecondsunboundedskip if more than this delay
successfulJobsHistoryLimit3succeeded Jobs to retain
failedJobsHistoryLimit1failed Jobs to retain
suspendfalsepause without deleting
backoffLimit6Pod retries before Job marked Failed
activeDeadlineSecondsunsethard cap on Pod runtime
ttlSecondsAfterFinishedunsetauto-delete the Job after this many seconds

Two common pitfalls. Forgetting timeZone makes the schedule follow the kube-controller-manager’s host timezone, which is unpredictable on managed Kubernetes. And on a one-minute schedule, the default successfulJobsHistoryLimit: 3 accumulates three Job objects per minute unless ttlSecondsAfterFinished is set.

Cross-platform cron equivalents

macOS launchd. Apple recommends launchd instead of cron. A launchd job is a .plist in ~/Library/LaunchAgents/:

<plist version="1.0"><dict>
  <key>Label</key><string>com.example.daily</string>
  <key>ProgramArguments</key><array><string>/usr/local/bin/daily.sh</string></array>
  <key>StartCalendarInterval</key>
  <dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>0</integer></dict>
</dict></plist>

Load with launchctl load ~/Library/LaunchAgents/com.example.daily.plist. Unlike cron, launchd catches up missed runs after sleep and wake.

Windows Task Scheduler uses schtasks:

schtasks /create /tn "DailyReport" /tr "C:\scripts\report.bat" /sc DAILY /st 09:00
schtasks /create /tn "EveryFifteen" /tr "C:\scripts\poll.bat" /sc MINUTE /mo 15

On WSL, native Linux cron works but stops when the session ends, so use Task Scheduler to launch always-on WSL jobs.

Cron in Docker containers. Most slim images (alpine, debian-slim, distroless) ship without a cron daemon. Install cronie or busybox-cron and run it as PID 1 with tini or s6-overlay. In most cases, using a Kubernetes CronJob instead is the better answer.

Advanced tips and patterns

Last day of month

Cron has no native “last day” operator. Run every day in the 28-31 window and check whether tomorrow is the 1st:

0 23 28-31 * *  [ "$(date -d tomorrow +\%d)" = "01" ] && /usr/local/bin/eom.sh

Nth weekday of the month

“First Monday” uses the same wrapper pattern: restrict to days 1-7, then check the weekday inside the command:

0 9 1-7 * *  [ "$(date +\%u)" = "1" ] && /usr/local/bin/first-monday.sh

For “last Friday”, use days 25-31 plus the day-of-week check.

Random offset to spread load

When many machines run the same cron, 0 0 * * * produces a thundering herd at midnight UTC. Add a random delay before the work runs:

RANDOM_DELAY=10                                              # cronie / anacron, in minutes
0 0 * * *  /usr/local/bin/job.sh

0 0 * * *  sleep $((RANDOM \% 600)); /usr/local/bin/job.sh   # portable

Heartbeat monitoring

Cron fails silently. The dead-man’s-switch pattern is to have the job ping a monitoring service after each successful run, and have the service alert when an expected ping does not arrive. Healthchecks.io, Cronitor, and Dead Man’s Snitch all offer free tiers.

*/15 * * * *  /usr/local/bin/job.sh && curl -fsS --retry 3 https://hc-ping.com/your-uuid

For monitoring logic that branches on response codes (200 healthy, 429 rate-limited, 503 degraded), see our HTTP status codes cheat sheet.

Idempotency is a property of the job, not the scheduler

Cron does not retry, recover missed runs, or guard against concurrent runs. The most reliable fix is to make the job itself safe to run multiple times. Instead of “send today’s report at 9 AM”, design it as “send today’s report if it has not been sent yet”. Missed runs, duplicates, and manual catch-ups all converge to the same state.

FAQ

Is */5 * * * * really every 5 minutes?

Almost — */5 * * * * is anchored to minute 0, not “every 5 minutes from now”. The expression fires at minute 0, 5, 10, …, 55 of every hour. The step */N is relative to the field’s lowest value, not the current time. Save at 12:03 and the next run is 12:05, not 12:08.

What does 0 0 * * * mean in cron?

0 0 * * * means every day at midnight (00:00) in the server’s local timezone. The fields read: minute 0, hour 0, any day-of-month, any month, any day-of-week. It is equivalent to the macros @daily or @midnight. To pin the timezone, add CRON_TZ=America/New_York at the top of the crontab.

How do I run a cron job every 30 seconds?

You cannot do it with standard POSIX cron, because the minimum granularity is one minute. Three workarounds exist: two staggered jobs at * * * * * with sleep 30 && on one, a systemd timer with OnCalendar=*:*:0/30, or a long-running worker that sleeps between iterations. The last option is usually the right one.

What timezone does cron use by default?

The server’s local system timezone, as resolved from /etc/timezone or the TZ environment variable. A 9 AM cron on a UTC server fires at 4 AM US East. Fix: set CRON_TZ= at the top of the crontab, or move all servers to UTC and design schedules in UTC. GitHub Actions is always UTC, and Kubernetes 1.27 and later supports spec.timeZone.

Why is my cron job not running?

If your cron job is not running, check the following in order: is the cron daemon up (systemctl status cron), is $PATH set in the crontab, is stderr captured (>> log 2>&1), is the user’s crontab loaded (crontab -l), is % escaped in commands, and is the timezone what you expect. Most “not running” reports land on the second or third item.

Is Kubernetes CronJob syntax the same as Linux cron?

Yes for the schedule field. Both use POSIX five-field cron. Kubernetes adds spec.timeZone (1.27 and later), concurrencyPolicy for overlap control, startingDeadlineSeconds for missed-run recovery, and suspend: true to pause. Linux cron has none of these. Reach for flock and anacron to fill the gaps.

What is the difference between @reboot and @daily?

@daily is a macro for 0 0 * * *, which runs every day at midnight on a fixed schedule. @reboot runs once when the cron daemon starts and has no recurring schedule. @reboot is supported by vixie cron and cronie, but not by Kubernetes CronJob, GitHub Actions, or AWS EventBridge. In containers, @reboot rarely fires.

What is the difference between cron and crontab?

Cron is the background daemon that runs scheduled jobs; crontab is the file that lists them (and the crontab command to edit it). The daemon reads each user’s crontab on a schedule and runs commands whose execution time matches the cron expression. So cron is the engine, crontab is the recipe.

Related Articles

View all articles