Vantly
All developer tools

Crontab generator

Build the expression, and read back in plain words when it will run.

0

minute

9

hour

*

day of the month

*

month

1-5

day of the week

Runs at 09:00, on Monday, Tuesday, Wednesday, Thursday and Friday.

Common ones

Docs

Why did my cron job run every minute instead of every hour?

Almost always an empty first field or a missing minute value. What the five fields mean, and the mistakes that make a job fire far more often than intended.

Almost always because the minute field is a * when it should be a number. An expression like * */2 * * * doesn't mean every two hours. It means every minute during every second hour, so it fires thirty times an hour rather than once.

The fix is 0 */2 * * *. Pin the smaller units, or they run through their whole range.

The five fields

┌─ minute        0 to 59
│ ┌─ hour        0 to 23
│ │ ┌─ day       1 to 31
│ │ │ ┌─ month   1 to 12
│ │ │ │ ┌─ weekday  0 to 6, Sunday is 0
│ │ │ │ │
* * * * *

Left to right, smallest to largest. Every field you leave as * means "all values", and that's where the surprises come from.

The characters

* every value. 5 exactly that value. 1-5 a range. 1,3,5 a list. */15 every fifteenth, so minutes 0, 15, 30 and 45.

The step is worth understanding properly. */7 on minutes gives 0, 7, 14, 21, 28, 35, 42, 49, 56, and then the hour rolls over and the next is 0. So the last gap is four minutes, not seven. Steps that don't divide their range evenly always have an uneven join at the top.

Use divisors when regularity matters: 1, 2, 3, 4, 5, 6, 10, 12, 15, 20 and 30 all divide 60 cleanly.

Day of month and day of week together

The trap that catches everyone. If both are set to something other than *, most cron implementations treat it as or, not and.

So 0 0 13 * 5 doesn't mean "Friday the 13th". It means "the 13th of any month, and also every Friday". If you want Friday the 13th specifically, you need the day check inside the script.

When one is * the behaviour is what you'd expect, which is why this only bites in the specific case where both are set.

Times worth avoiding

Midnight. Everybody schedules at 0 0 * * *, so backups, reports and cleanups across the world all start at once. If the job touches a shared service, pick a minute nobody else picked. 17 3 * * * is no harder to write than 0 0 * * *.

The hour boundary generally. Minute 0 is congested for the same reason.

Between 1am and 3am, if your server observes daylight saving. That window either happens twice or not at all, twice a year. A job scheduled at 2:30am either runs twice or gets skipped, and which one depends on the direction.

For anything where that matters, run the server in UTC. It's the single easiest way to remove a whole class of scheduling bugs.

Which minute the job actually starts

Cron fires at the start of the minute, but it doesn't guarantee an instant start under load, and it makes no attempt to space out jobs that share a time.

If ten jobs are scheduled at 0 * * * *, all ten start together and compete for the same CPU and disk. Staggering them by a few minutes usually costs nothing and removes the contention.

Overlapping runs

Cron will happily start a new run while the previous one is still going. A job scheduled every five minutes that sometimes takes seven will eventually have several copies running at once, and if it writes anywhere shared, that's a corruption waiting to happen.

Cron has no protection against this. Use a lock file, or a tool like flock, and decide explicitly whether a late run should be skipped or queued.

The special strings

Most implementations accept @daily, @hourly, @weekly, @monthly and @reboot as shorthands. They're readable, and @daily means midnight, which puts you back in the congested slot.

@reboot runs once when the machine starts, which is occasionally what people actually wanted when they wrote a complicated expression.

Before you trust it

Cron's environment is not your shell's. It typically has a minimal PATH, no profile loaded, and a different working directory. A script that works when you run it and fails under cron is nearly always one of those three.

Use absolute paths for everything, set any variables the script needs inside the script, and send the output somewhere you'll see it. A job that fails silently at 3am fails silently for months.