Cron Expressions Explained — Scheduling Done Right
Cron syntax, common patterns, and platform-specific quirks for Linux, GitHub Actions, Kubernetes, and AWS.

You need a DB backup at 3 AM every night. Or cache invalidation every hour. Or a weekly report email on Monday mornings. Cron handles all of that. It's the backbone of task scheduling on Linux, and the same expression syntax shows up in GitHub Actions, Kubernetes CronJobs, and AWS EventBridge.
The 5-Field Format
A standard cron expression has five fields:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-6, 0=Sunday)
│ │ │ │ │
* * * * *
Each field supports these operators:
| Symbol | Meaning | Example |
|---|---|---|
* | Every value | * * * * * = every minute |
| number | Specific value | 30 9 * * * = 9:30 AM daily |
, | Multiple values | 0,30 * * * * = top and bottom of every hour |
- | Range | 1-5 = 1 through 5 |
/ | Step | */10 * * * * = every 10 minutes |
Common Patterns
Time-Based
* * * * * every minute
*/5 * * * * every 5 minutes
0 * * * * top of every hour
0 */2 * * * every 2 hours
30 9 * * * 9:30 AM daily
0 0 * * * midnight daily
0 9,18 * * * 9 AM and 6 PM daily
Day-of-Week
0 9 * * 1 Monday at 9 AM
0 9 * * 1-5 weekdays at 9 AM
0 0 * * 0 Sunday at midnight
0 18 * * 5 Friday at 6 PM
Date-Based
0 0 1 * * first of every month at midnight
0 0 1,15 * * 1st and 15th at midnight
0 9 1 1 * January 1st at 9 AM
0 0 * * 1#1 first Monday of the month (some systems)
Working with crontab
On Linux, crontab manages your scheduled jobs.
# list current user's cron jobs
crontab -l
# edit cron jobs
crontab -e
# view another user's cron jobs (requires root)
sudo crontab -u www-data -l
Inside the crontab file, one job per line:
# DB backup at 3 AM daily
0 3 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1
# health check every 10 minutes
*/10 * * * * curl -s https://mysite.com/health > /dev/null
That >> /var/log/backup.log 2>&1 part redirects both stdout and stderr to a log file. Skip it and cron might try to send the output as email — not what you want on most systems.
Gotchas
Timezone. Cron runs in the system's timezone. A server set to UTC means 0 9 * * * fires at 9 AM UTC, not your local time.
timedatectl # check system timezone
Environment variables. Cron doesn't load your shell profile. PATH, HOME, and other variables might differ from what you expect. Always use absolute paths in your scripts.
# set environment variables at the top of crontab
PATH=/usr/local/bin:/usr/bin:/bin
Overlapping runs. If a job takes longer than the interval between runs, executions can pile up. Use flock to prevent that:
*/5 * * * * flock -n /tmp/myjob.lock /home/user/scripts/slow-job.sh
Cron in GitHub Actions
GitHub Actions uses cron syntax in its schedule trigger.
on:
schedule:
- cron: '0 9 * * 1' # Monday at 9 AM UTC
Two things to watch out for: the timezone is always UTC, and there's no guarantee of exact timing. Under heavy load, scheduled workflows can be delayed by minutes or even longer.
Kubernetes CronJobs
Same expression syntax, different context:
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-backup
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
containers:
- name: backup
image: my-backup-image
restartPolicy: OnFailure
The concurrencyPolicy field controls what happens when the previous run is still going: Allow (run both), Forbid (skip the new one), or Replace (kill the old one and start fresh).
AWS EventBridge (CloudWatch Events)
AWS uses a 6-field format — it adds a year field:
cron(minute hour day month day-of-week year)
cron(0 9 ? * MON-FRI *) # weekdays at 9 AM UTC
The ? means "no specific value" and must appear in either the day-of-month or day-of-week field. The syntax diverges from standard cron in a few other ways too, so always double-check the AWS docs when writing these.
Debugging
When a cron job isn't running, here's the checklist:
# is the cron service running?
systemctl status cron
# check cron execution logs
grep CRON /var/log/syslog
# make sure the script is executable
chmod +x /home/user/scripts/backup.sh
Permission issues are the most common culprit. If the script runs fine manually but fails under cron, it's almost always a PATH or file permission problem.
To verify your expression does what you think it does before deploying, try a Cron Expression Generator. Set each field through the UI and it'll show you the next execution times. Handy for complex expressions like */15 3-6 * * 1-5.