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:.
| Schedule | Cron Expression | Plain 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 hour | 0 * * * * | top of every hour |
| Every 2 hours | 0 */2 * * * | hour 0, 2, 4, …, 22 |
| Every 6 hours | 0 */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 AM | 0 9 * * 1-5 | Mon-Fri 09:00 |
| Every weekend at 9 AM | 0 9 * * 0,6 | Sat and Sun 09:00 |
| Daily at midnight | 0 0 * * * | every day 00:00 |
| Daily at 2:30 AM | 30 2 * * * | low-traffic batch window |
| Every Monday at 9 AM | 0 9 * * 1 | Mondays 09:00 |
| Every Friday at 5 PM | 0 17 * * 5 | Fridays 17:00 |
| Every Sunday at midnight | 0 0 * * 0 | equivalent to @weekly |
| First of month at midnight | 0 0 1 * * | 1st day 00:00 — equivalent to @monthly |
| 15th of every month at noon | 0 12 15 * * | mid-month payroll window |
| Last day check (wrapper) | 0 0 28-31 * * + script | requires 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 @yearly | new year midnight |
| Every 5 min on weekdays 9-5 | */5 9-17 * * 1-5 | business-hours polling |
| Every 30 min on weekends | */30 * * * 0,6 | Sat/Sun monitoring |
| Twice an hour, 15 and 45 | 15,45 * * * * | offset from the :00 stampede |
| First Monday (wrapper) | 0 9 1-7 * 1 + AND check | wrapper needed (see below) |
| Macros | @hourly @daily @weekly @monthly @yearly | non-standard but widely supported |
| On reboot only | @reboot | non-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
| Field | Range | Aliases | Notes |
|---|---|---|---|
| Minute | 0-59 | none | 0 means “on the hour” |
| Hour | 0-23 | none | 24-hour clock; 0 is midnight, 12 is noon |
| Day-of-month | 1-31 | none | invalid days for a month silently never fire (Feb 31) |
| Month | 1-12 | JAN, FEB, MAR, …, DEC | case-insensitive |
| Day-of-week | 0-7 | SUN, MON, TUE, …, SAT | both 0 and 7 mean Sunday |
Operators in detail
Five operators cover every standard cron expression:
| Operator | Meaning | Example | Expands to |
|---|---|---|---|
* | any value | * * * * * | every minute |
, | list | 0 9,12,17 * * * | 09:00, 12:00, 17:00 |
- | inclusive range | 0 9-17 * * * | every hour 09:00 to 17:00 |
/ | step | */15 * * * * | minute 0, 15, 30, 45 |
| mixed | combined | 0 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:
| Macro | Expands to | Meaning |
|---|---|---|
@yearly / @annually | 0 0 1 1 * | once a year, Jan 1 at midnight |
@monthly | 0 0 1 * * | first of each month at midnight |
@weekly | 0 0 * * 0 | every Sunday at midnight |
@daily / @midnight | 0 0 * * * | every day at midnight |
@hourly | 0 * * * * | 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.
| Feature | vixie cron | systemd timer | K8s CronJob | GHA schedule | AWS EventBridge | Vercel Cron | Cloudflare Workers |
|---|---|---|---|---|---|---|---|
| Field syntax | 5-field POSIX | OnCalendar spec | 5-field POSIX + timeZone | 5-field POSIX | 6-field Quartz with ? | 5-field POSIX | 5-field POSIX |
| Minimum interval | 1 minute | 1 second | 1 minute | best-effort, ≥15 min recommended | 1 minute | 1 minute (Pro plan) | 1 minute |
| Explicit timezone | CRON_TZ= | Persistent=true | spec.timeZone (1.27+) | UTC only | ScheduleExpressionTimezone | UTC only | UTC only |
| Missed-run recovery | no (use anacron) | yes (Persistent=true) | yes (startingDeadlineSeconds) | no | yes | no | no |
| Retry / backoff | no | partial | yes (backoffLimit) | retry on failure | yes | no | yes |
| Concurrency control | no (use flock) | partial | yes (concurrencyPolicy) | no | no | no | no |
@reboot support | yes | yes (via OnBootSec=) | no | no | no | no | no |
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.
| Field | Default | What it does |
|---|---|---|
schedule | required | POSIX 5-field cron expression |
timeZone | controller TZ | explicit timezone (1.27+); use IANA names |
concurrencyPolicy | Allow | Forbid skips new runs while previous active; Replace cancels it |
startingDeadlineSeconds | unbounded | skip if more than this delay |
successfulJobsHistoryLimit | 3 | succeeded Jobs to retain |
failedJobsHistoryLimit | 1 | failed Jobs to retain |
suspend | false | pause without deleting |
backoffLimit | 6 | Pod retries before Job marked Failed |
activeDeadlineSeconds | unset | hard cap on Pod runtime |
ttlSecondsAfterFinished | unset | auto-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.