正确理解和编写 cron 表达式
Cron expressions are a strange mix of brilliant and cryptic. Five fields with digits and special characters are enough to describe arbitrarily complex schedules — from a daily backup at 03:15 to "every other Tuesday of the quarter." Once you internalize the syntax, you never want to leave it. If you don't, it looks like magic. This article clears it up.
A short history
cron goes back to Unix Version 7 in 1979. Written by Brian Kernighan, it was a simple daemon that read a config file every minute and ran matching commands. In 1987, Paul Vixie wrote the variant in widespread use today (Vixie cron), adding user crontabs, environment variables, and the @reboot keyword.
Today there are many cron dialects: GNU/Linux distros usually ship Vixie cron or a derivative. macOS used cron for a long time but now uses launchd. Kubernetes has CronJobs with Unix syntax. Java frameworks like Quartz introduced an extended 6/7-field dialect. The core ideas are similar — details differ.
The 5-field syntax
A classic Unix cron expression consists of five whitespace-separated fields, followed by the command to run. The fields, in order:
- Minute (0–59)
- Hour (0–23)
- Day of month (1–31)
- Month (1–12 or JAN–DEC)
- Day of week (0–6, 0 = Sunday, or SUN–SAT)
Special characters
Each field supports several special characters to describe value ranges:
- * — any value. In the minute field, * means every minute.
- , — list of values. Example: 0,30 in the minute field means minute 0 and minute 30.
- - — range. Example: 9-17 in the hour field means 09:00, 10:00, ..., 17:00.
- / — step. Example: */15 in the minute field means every 15 minutes (0, 15, 30, 45). Combinable with ranges: 0-30/5 means 0, 5, 10, ..., 30.
- ? — "no specific value". Only supported by Quartz, used in either the day-of-month or day-of-week field when the other has been specified.
Aliases
Instead of the five fields, you can use aliases — shorter and more readable:
- @yearly / @annually — equivalent to 0 0 1 1 * (once a year, at midnight on January 1)
- @monthly — 0 0 1 * * (the 1st of every month at midnight)
- @weekly — 0 0 * * 0 (every Sunday at midnight)
- @daily / @midnight — 0 0 * * * (every day at midnight)
- @hourly — 0 * * * * (every hour, on the hour)
- @reboot — once at cron daemon start (typically at system boot)
Practical examples
- */15 * * * * — every 15 minutes
- 0 3 * * 0 — every Sunday at 03:00
- 30 8 * * 1-5 — Monday to Friday at 08:30
- 0 0 1 */3 * — on the 1st of every quarter at midnight
- 15 9-17 * * 1-5 — Monday to Friday, at minute 15 of each hour between 9 and 17
Quartz and other extensions
Java schedulers like Quartz use an extended syntax with six or seven fields: an additional seconds field at the front, optionally a year field at the end. There are also special characters like L (last day of month), W (nearest weekday), and # (nth occurrence of weekday in month). Quartz also allows ?, which Unix cron doesn't know.
If you copy a cron expression from a Java/Spring tutorial into a Linux crontab, the extra seconds field can be parsed as a minute — and the job runs at completely different times. Before porting, always check the target platform's documentation.
Common pitfalls
- Time zone: cron uses the system's local timezone by default. If you run servers worldwide, set a timezone explicitly (e.g. via CRON_TZ in Vixie cron) — otherwise the "nightly backup" suddenly runs in the afternoon of your region on Tokyo servers.
- DST transitions: on days with clock changes, a job can be skipped entirely in a missing hour, or run twice in a doubled hour. If that's not acceptable, use UTC as the cron timezone.
- Slow jobs: cron starts the next job at its scheduled time, regardless of whether the previous one is still running. For overlapping backups or syncs, use a lock file or a wrapper like flock.
- DoM and DoW are combined with OR, not AND: 0 0 15 * 1 runs both on the 15th of any month and on every Monday. If you want "only Mondays that are the 15th," you need either an extra script check or a Quartz-like syntax.
Frequently asked questions
How do I test a cron expression without waiting?
Cron generators and parsers can predict the next few execution times of an expression. On servers, you can test the command with a short cron entry (e.g. * * * * *) and remove it after a successful run.
Why isn't my cron job running?
Classics: PATH and env vars under cron are usually minimal. Use absolute paths (/usr/bin/python3, not python3), check the cron user's mailbox for errors, or redirect stdout/stderr to a log file. Permissions and the correct user in the crontab header are often overlooked, too.
Do I still need cron today?
For many scenarios there are alternatives: systemd timers on modern Linux, Kubernetes CronJobs in containers, Lambda schedules in the cloud. But cron is everywhere, well understood, and works without extra infrastructure — for a simple, regular task on a server, it's often the most pragmatic choice.