Stateless Tools

Five fields, and that is all

Minute, hour, day of month, month, day of week. Rather than memorising the order, it helps to understand that each field is not "when" but "which values match". Cron wakes up every minute, checks whether the current time satisfies all five fields, and runs the job if it does.

An asterisk means "any value"

30 * * * * only constrains the minute, so it runs at :30 of every hour. It does not mean "hourly" — it means you only set a minute condition.

Step values are modulo, not intervals

*/15 matches minutes divisible by 15, so 0, 15, 30 and 45. 7 */3 * * * is not "every three hours from now" but 00:07, 03:07, 06:07 and so on.

Ranges and lists combine

0 9-18/2 * * 1-5 means weekdays, on the hour, every two hours from 09:00 to 18:00. Commas list individual values, as in 0 0,12 * * *.

That is the syntax. Everything below is where real incidents come from. Paste an expression into the cron expression parser as you read to see the upcoming run times.

Day-of-month and day-of-week are OR, not AND

This is the most common cron mistake, and because it is not a syntax error the job simply runs on a different schedule than you expected.

What actually happens

When both the day-of-month and day-of-week fields are set to something other than *, most cron implementations combine them with OR. So 0 0 1 * 1 is not "the 1st, when it is a Monday" but "the 1st or any Monday". A job you expected to run monthly runs about five times a month.

Why it works that way

It is the intended behaviour of Vixie cron and it is specified in POSIX. The purpose was to express two schedules on one line, such as "back up on the 1st and every Friday". The problem today is that most people expect AND without knowing this rule.

What to do instead

Keep one of the two fields as *. Use 0 0 1 * * for monthly and 0 0 * * 1 for weekly. If you genuinely need AND, schedule broadly in cron and exit early in the script after checking the date. A condition in your own code is safer than relying on scheduler-specific behaviour.

Does it differ between implementations?

Vixie cron and cronie use OR. Quartz and some cloud schedulers use a different syntax entirely, requiring ? in one of the two fields and forbidding both at once. So when moving an expression to another runtime, verify it against that runtime's documentation instead of copying the string.

Time zones: the expression is right but the clock is not

Containers usually run in UTC

Most official Linux images start in UTC. If you write 0 9 * * * meaning "9am", it fires at 18:00 Korean time. A schedule that was correct locally slips by nine hours after deployment.

Check it with one command

Run date inside that environment. Support for TZ= or CRON_TZ= in the crontab varies by implementation, and where it exists it often applies only to lines after the declaration.

Daylight saving makes it worse

Korea has no daylight saving, but in US or EU regions a given wall-clock time can be skipped or repeated on transition days. That is why scheduling between 02:00 and 03:00 is usually avoided.

Verify the intended times with the upcoming-run list in the cron expression parser, and use the Unix timestamp converter when you need to line up epoch values with local time.

Overlap, duplication and silent failure

Three ways a correct schedule still breaks in production, most often on jobs whose runtime approaches their interval.

The next run starts before the last one finished

Cron does not check whether the previous process is still alive. A job scheduled */5 that starts taking seven minutes will pile up until connections or memory run out. Take a lock with flock -n, or check a PID file and exit if the job is already running. Skipping is usually safer than retrying.

More instances means more runs

Scaling a container that ships the same crontab to three replicas runs the job three times: three notifications, three settlement writes. Put the scheduler somewhere independent of replica count — a single dedicated worker or a managed scheduler — or make the job idempotent.

Failures produce nothing at all

Cron tries to mail stdout and stderr, and on a server without mail configured that output simply disappears. Without >> /var/log/job.log 2>&1 you have no way to see a failure. Better still, emit a heartbeat at the end of the job and alert when it stops arriving — a job that never ran is far harder to notice than one that errored.

PATH and environment differ from your shell

Cron runs with a minimal environment, which is why a command that works in your terminal reports "command not found" under cron. Use absolute paths or load the environment explicitly at the top of the script. Python virtual environments are a frequent offender.

A checklist before you deploy

  1. Paste the expression into the parser and confirm the next five run times match your intent.
  2. Check that one of the day-of-month and day-of-week fields is *. If both are set, the schedule is an OR.
  3. Run date in the deployment environment to confirm the time zone.
  4. Confirm the job's worst-case runtime is shorter than its interval, or take a lock.
  5. Consider whether the number of instances could grow.
  6. Redirect output to a log, and make sure you are alerted when the job does not run.
  7. Use absolute paths and load required environment variables inside the script.

Next: verify expressions with the cron expression parser, read epoch values from logs with the Unix timestamp converter, and line up intervals and settlement dates with the date calculator.