Master cron expressions with this complete guide. Learn cron syntax, common schedules, real-world examples, and use a free cron expression generator.
The first time I wrote a cron expression from memory, I got the day-of-week field wrong. I thought Sunday was 0. It is — on Linux. But in the cron implementation my CI/CD platform used, Sunday was 7. My "run every Sunday at midnight" job ran on Saturday instead, and the weekly database backup happened a full day early. Nobody noticed for three weeks.
Cron expressions look simple. Five fields, some numbers, maybe an asterisk or two. But the details matter, and the details vary between systems. This guide covers everything I wish someone had told me before I started scheduling tasks in production.
A cron expression is a string that defines a schedule. It tells a system exactly when to run a task — down to the minute. The name comes from cron, the time-based job scheduler in Unix-like operating systems, which itself is named after Chronos, the Greek word for time.
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–7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * *
Each field accepts numbers, special characters, or a combination of both. The scheduler checks the current time against the expression every minute. If all five fields match, the task runs.
Here's a reference table for every field in a cron expression:
| Field | Allowed Values | Allowed Special Characters |
|---|---|---|
| Minute | 0–59 | , - * / |
| Hour | 0–23 | , - * / |
| Day of Month | 1–31 | , - * / ? L W |
| Month | 1–12 or JAN–DEC | , - * / |
| Day of Week | 0–7 or SUN–SAT | , - * / ? L # |
Some implementations add a sixth field for seconds (0–59) at the beginning, or a seventh field for year at the end. The standard five-field format is what you'll encounter in Linux crontab, most CI/CD platforms, and the majority of scheduling libraries.
These are the building blocks of any cron schedule. Understanding them is the difference between a working schedule and a 3 AM incident.
Asterisk (*) — Matches every value in the field. * * * * * means every minute of every hour of every day.
Comma (,) — Lists multiple values. 1,15 * * * * means at minute 1 and minute 15 of every hour.
Hyphen (-) — Defines a range. 9-17 * * * * is wrong — that sets the minute field to 9 through 17. What you probably meant was * 9-17 * * *, which means every minute from 9 AM to 5 PM.
Slash (/) — Defines a step. */5 * * * * means every 5 minutes. 0 */2 * * * means every 2 hours, at minute 0. The number before the slash is the start, the number after is the interval. 10/15 * * * * means starting at minute 10, then every 15 minutes (10, 25, 40, 55).
Question mark (?) — Used in some implementations (like Quartz Scheduler) for day-of-month or day-of-week to mean "no specific value." Standard Linux cron doesn't use this.
L — Last. L in the day-of-month field means the last day of the month. 5L in the day-of-week field means the last Friday.
W — Nearest weekday. 15W in the day-of-month field means the nearest weekday to the 15th.
# — Nth occurrence. 5#3 in the day-of-week field means the third Friday of the month.
These are the cron expressions I use most. I've typed them enough times that they're muscle memory, but I still double-check them with a Cron Expression Generator before deploying.
| Schedule | Cron Expression |
|---|---|
| Every minute | * * * * * |
| Every 5 minutes | */5 * * * * |
| Every 15 minutes | */15 * * * * |
| Every hour (at minute 0) | 0 * * * * |
| Every day at midnight | 0 0 * * * |
| Every day at 6 AM | 0 6 * * * |
| Every Monday at 9 AM | 0 9 * * 1 |
| Every weekday at 8 AM | 0 8 * * 1-5 |
| First day of every month at noon | 0 12 1 * * |
| Every Sunday at 2 AM | 0 2 * * 0 |
| Every 6 hours | 0 */6 * * * |
| Twice a day (8 AM and 8 PM) | 0 8,20 * * * |
| Every quarter (Jan, Apr, Jul, Oct 1st) | 0 0 1 1,4,7,10 * |
One thing I see developers get wrong consistently: */30 * * * * does not mean "every 30 minutes starting from now." It means at minute 0 and minute 30 of every hour. Cron doesn't care when you saved the file. It matches the clock.
Here's what a production crontab file actually looks like. Not the clean textbook version — the one with environment variables, output redirection, and the inevitable comment explaining why something runs at 3:17 AM instead of 3:00 AM.
# Set environment
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com
# Database backup — daily at 2 AM
0 2 * * * /opt/scripts/db-backup.sh >> /var/log/db-backup.log 2>&1
# Clean temp files — every 6 hours
0 */6 * * * find /tmp -type f -mtime +7 -delete
# SSL certificate renewal check — twice daily
0 6,18 * * * /opt/scripts/check-ssl.sh
# Cache warmup — every 15 minutes during business hours
*/15 9-17 * * 1-5 curl -s https://example.com/api/cache-warm > /dev/null
# Monthly report — 1st of month at 8 AM
0 8 1 * * /opt/scripts/monthly-report.sh
# Log rotation — Sunday at 4 AM
# Runs at 4:17 to avoid collision with the monitoring check at 4:00
17 4 * * 0 /opt/scripts/rotate-logs.shThat last comment is real. If you have multiple cron jobs, stagger them. Running five heavy tasks at exactly 0 0 * * * will spike your CPU and memory at the same time. Offset them by a few minutes.
The classic. Edit your crontab with crontab -e, list it with crontab -l. System-wide cron jobs go in /etc/crontab or /etc/cron.d/. The key difference with system crontab files is that they include a user field between the schedule and the command:
# System crontab format (note the username field)
0 2 * * * root /opt/scripts/db-backup.shThere are also shortcut strings that some cron implementations support:
| Shortcut | Equivalent |
|---|---|
@reboot | Run once at startup |
@hourly | 0 * * * * |
@daily | 0 0 * * * |
@weekly | 0 0 * * 0 |
@monthly | 0 0 1 * * |
@yearly | 0 0 1 1 * |
If you're building a web application, you'll probably schedule tasks within your application process rather than using the system crontab. Two popular libraries handle this:
// Using node-cron
import cron from "node-cron";
// Run every day at midnight
cron.schedule("0 0 * * *", () => {
console.log("Running daily cleanup...");
cleanupExpiredSessions();
});
// Run every Monday at 9 AM
cron.schedule("0 9 * * 1", () => {
sendWeeklyReport();
});// Using node-schedule (supports 6-field expressions with seconds)
import schedule from "node-schedule";
// Run at second 0, every 30 minutes
const job = schedule.scheduleJob("0 */30 * * * *", () => {
checkHealthEndpoints();
});
// Cancel the job later if needed
job.cancel();One important caveat: in-process schedulers die when your process dies. If your Node.js server restarts, pending jobs are lost. For critical tasks, use an external scheduler or a job queue with persistence.
GitHub Actions, GitLab CI, and other platforms support cron-triggered workflows. The syntax is identical to standard cron, but the context is different — you're triggering a pipeline run, not executing a command directly.
# GitHub Actions — .github/workflows/nightly.yml
name: Nightly Build
on:
schedule:
- cron: "0 3 * * *" # Every day at 3 AM UTC
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm testCritical detail: GitHub Actions cron schedules run in UTC. Not your local timezone, not your server's timezone. UTC. I've seen teams schedule a "nightly" build at 0 0 * * * thinking it runs at midnight local time, when it actually runs at midnight UTC — which might be the middle of their workday.
Kubernetes has a built-in CronJob resource that creates Job objects on a schedule:
apiVersion: batch/v1
kind: CronJob
metadata:
name: db-cleanup
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
jobTemplate:
spec:
template:
spec:
containers:
- name: cleanup
image: myapp/cleanup:latest
command: ["node", "cleanup.js"]
restartPolicy: OnFailureThe concurrencyPolicy: Forbid setting is important. Without it, if a job takes longer than the interval, a new instance starts while the previous one is still running. For database operations, that's usually catastrophic.
1. Always use a cron expression generator for complex schedules. I build and maintain a Cron Expression Generator specifically because I got tired of making mistakes. Type in the expression, see the next 10 run times, confirm it does what you think it does.
2. Log everything. Cron jobs run silently by default. If they fail, you won't know unless you explicitly capture output. Always redirect stdout and stderr to a log file, or use a monitoring service.
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&13. Use absolute paths. Cron jobs run with a minimal environment. PATH might not include /usr/local/bin where your Node.js or Python binary lives. Use full paths to executables and scripts.
4. Set timeouts. A cron job that hangs is worse than one that fails. Wrap long-running tasks with timeout:
0 2 * * * timeout 3600 /opt/scripts/backup.shThis kills the backup if it runs longer than one hour.
5. Handle overlapping executions. If a job runs every 5 minutes but sometimes takes 7 minutes, you'll have two instances running simultaneously. Use a lock file:
#!/bin/bash
LOCKFILE="/tmp/myjob.lock"
if [ -f "$LOCKFILE" ]; then
echo "Already running, exiting."
exit 0
fi
trap "rm -f $LOCKFILE" EXIT
touch "$LOCKFILE"
# ... actual work here6. Test with short intervals first. Don't set a job to run monthly and then wait a month to see if it works. Test with * * * * * (every minute), verify it runs correctly, then switch to the real schedule.
7. Mind the timezone. Your server's timezone and your timezone might differ. Many cloud platforms run in UTC. Check with date or timedatectl before writing your schedule.
When a cron job doesn't run, the issue is almost always one of these:
The environment is different. Cron doesn't load your .bashrc or .profile. Environment variables you set in your shell session don't exist in cron. Test your script by running it with env -i /bin/bash /path/to/script.sh to simulate the minimal cron environment.
Permissions are wrong. The crontab file itself must be owned by the user and have correct permissions. The script it runs must be executable. Check both.
The cron daemon isn't running. Yes, this happens. Check with systemctl status cron or service cron status.
Mail is silently failing. By default, cron tries to email output to the user. If the mail system isn't configured, that output just vanishes. Always redirect to a log file explicitly.
The expression is wrong. This is the most common cause. You can verify your cron expression with tools like the Cron Expression Generator, or test it with echo "next run:" && date in a temporary cron entry.
* = every value
, = list separator (1,3,5)
- = range (1-5)
/ = step (*/5 = every 5)
@reboot = on startup
@hourly = 0 * * * *
@daily = 0 0 * * *
@weekly = 0 0 * * 0
@monthly = 0 0 1 * *
@yearly = 0 0 1 1 *
If you work with scheduled tasks and time-based operations, these tools pair well with cron:
Cron expressions are one of those things that seem too simple to study. Five fields, a few special characters, and you're scheduling tasks. But the edge cases — timezone mismatches, overlapping executions, missing environment variables, day-of-week numbering differences — are where production incidents hide.
Write the expression. Check it with a generator. Test it with a short interval. Log the output. Set a timeout. Handle overlaps. These six steps have saved me from every cron-related outage I might have had in the last few years.
The best cron job is the one you set up correctly the first time and never think about again. The worst one is the one that silently fails for three weeks before someone notices the backups stopped.