Cron mistakes, and what they actually run.
Every one of these is written on purpose by someone competent, and every one of them does something other than what it says. Each one links into the generator, whose next-run list shows the dates the expression really fires on: that list is the cheapest way to catch your own version of these.
The eight, at a glance.
| Written as | Read as | What it does instead | Detail |
|---|---|---|---|
| 0 0 1 * 1 | The first day of the month, and every Monday. | Runs on the 1st and on every Monday. | #1 |
| */5 * * * * | Five minutes after each previous run finishes. | Runs at minutes 0, 5, 10, 15 and so on, aligned to the clock. | #2 |
| 0 0 28-31 * * | The last day of the month. | Fires on the 28th, 29th, 30th and 31st, whichever of those days the month has. | #3 |
| 0 0 */14 * * | Every two weeks. | Fires on the 1st, the 15th and the 29th of any month long enough to have one. | #4 |
| 0 5 * * * | Every five hours. | Runs once a day, at 05:00. | #5 |
| 0 0 30 2 * | A monthly run at the end of February. | Nothing, ever. | #6 |
| 0 0 12 * * ? | Every day at noon, the way it is written in a Spring or Quartz service. | Nothing, in a crontab: six fields are a parse error, and a daemon that cannot parse the line runs none of it. | #7 |
| 0 0 * * * | Midnight where I live. | Midnight on the clock of the machine running the job, which is UTC on most servers: 03:00 in Kyiv in summer, 02:00 in winter, and a different hour again after a daylight-saving change. | #8 |
0 0 1 * 1
Read as: The first day of the month, and every Monday.
Actually does: Runs on the 1st and on every Monday. Standard cron reads a restricted day-of-month and a restricted day-of-week as "either one", not "both". People write this expecting an AND and get roughly five runs a month instead of one.
Fix: Pick one day rule per line: 0 0 1 * * for the 1st, 0 0 * * 1 for Mondays. If you truly need both, let the job check the date itself. Check it in the generator — more: the parser flags both day fields.
*/5 * * * *
Read as: Five minutes after each previous run finishes.
Actually does: Runs at minutes 0, 5, 10, 15 and so on, aligned to the clock. Nine minutes of work and a five-minute step means two jobs at once, every hour, on the hour.
Fix: Accept the grid and make the job safe to overlap with a lock (flock, a lock table, a single-worker queue), or move the cadence to a scheduler that measures intervals. Check it in the generator — more: every 5 minutes in full.
0 0 28-31 * *
Read as: The last day of the month.
Actually does: Fires on the 28th, 29th, 30th and 31st, whichever of those days the month has. In a 31-day month that is four runs for one intention.
Fix: Use 0 0 L * * where the scheduler understands L (Quartz does, a plain crontab does not), or keep 28-31 and make the job exit unless tomorrow is the 1st. Check it in the generator — more: the portable last-day version.
0 0 */14 * *
Read as: Every two weeks.
Actually does: Fires on the 1st, the 15th and the 29th of any month long enough to have one. Some months get three runs, February gets two, and the gap between the 29th and the next 1st is three days.
Fix: For twice a month on fixed dates use 0 0 1,15 * *. For a true two-week cadence, run weekly and let the job decide whether this is its week. Check it in the generator — more: the 1st and 15th expression.
0 5 * * *
Read as: Every five hours.
Actually does: Runs once a day, at 05:00. The number sits in the hour field, so it names an hour, not an interval.
Fix: Steps belong in the field they repeat in: 0 */5 * * * for every five hours, */5 * * * * for every five minutes. Check it in the generator — more: the same idea in the hour field.
0 0 30 2 *
Read as: A monthly run at the end of February.
Actually does: Nothing, ever. February has no 30th, and cron does not warn about a date that cannot arrive: the job is simply never due. This is the quietest failure in the list.
Fix: Name a date that exists (0 0 28 2 *), or use the last-day pattern with a guard. Check it in the generator — more: why the last day is the portable answer.
0 0 12 * * ?
Read as: Every day at noon, the way it is written in a Spring or Quartz service.
Actually does: Nothing, in a crontab: six fields are a parse error, and a daemon that cannot parse the line runs none of it. This is how a job disappears during the move from Java to a Linux box.
Fix: Drop the seconds field and map the ? token. The converter does both and says what could not be carried over. Check it in the generator — more: the Quartz converter.
0 0 * * *
Read as: Midnight where I live.
Actually does: Midnight on the clock of the machine running the job, which is UTC on most servers: 03:00 in Kyiv in summer, 02:00 in winter, and a different hour again after a daylight-saving change.
Fix: Set CRON_TZ (or the scheduler equivalent) on the crontab, or move the hour so it lands where you want it in the server timezone. Check it in the generator — more: midnight and the timezone that decides it.
Cron questions, answered.
Why does a cron job with a day of month and a day of week run twice?
Because standard cron treats the two day fields as alternatives. If both are restricted, the job runs when either one matches: 0 0 1 * 1 fires on the 1st of the month and on every Monday. To get an AND, leave one of the two fields as * and check the other condition inside the job.
Does */5 * * * * mean every five minutes?
It means every fifth minute of the hour, at minutes 0, 5, 10 and so on, on a grid set by the clock rather than by the previous run. If a run takes longer than the step, two copies overlap. Cron has no interval timer, so an exact five minutes after the last finish needs a lock or a different scheduler.
Why did my cron job never run?
The three quiet failures are an impossible date (0 0 30 2 *), a token the daemon does not understand (a six-field Quartz expression in a crontab, or L in Vixie cron), and a line whose command cannot be found because cron runs with a minimal PATH. None of them produce an error you would notice; the parse error usually goes to the mail of the user the crontab belongs to.
How do I check an expression before I trust it?
Read it field by field rather than eyeballing it: the parser on this site shows what each field means and flags the day-field trap, and the next-run list shows the dates it would actually fire on. Checking the first five real dates catches most of the mistakes on this page.
Reading an expression someone else wrote? The parser breaks it into fields and flags the day-field trap; the schedule pages cover the expressions people look up most; the converter handles the six-field ones.