Cron Job Helper — Tips & Tricks

Cron pitfalls: OR logic of the day fields, time zone and DST, % in a crontab, steps across the hour, MAILTO, absolute paths and locking with flock.

Back to the overview: Cron Job Helper · Open the live tool: www.jpkc.com/tools/cron/

The Manual explains every tab; the Examples show concrete expressions. This page is about what tends to go wrong when writing cron — the traps you only notice on the third missed run.

The infamous OR logic between day-of-month and day-of-week

The single most important cron trap: when you restrict both the Day of Month field and the Day of Week field (neither starts with *), classic Unix cron combines them with OR, not AND. The job then runs on every day that satisfies either condition.

Example:

0 0 13 * 5

This does not run "only on a Friday the 13th" but on every 13th of the month AND additionally every Friday. The Cron Job Helper makes that unmistakable: the plain text reads "At 00:00, on day 13 of the month OR on Friday (either one matches)", and the Next Scheduled Runs list every Friday and every 13th accordingly.

Practical takeaway: always restrict only one of the two day fields and leave the other on *. If you really want "Friday the 13th", restrict only the day in cron and check the weekday inside the command — following the same pattern the Reference tab shows for "last day of the month":

0 0 13 * * [ "$(date +\%u)" = 5 ] && /usr/local/bin/job.sh

date +%u returns the weekday as 1 (Monday) to 7 (Sunday); in a crontab, the % has to be written as \%. And watch out with steps: a field that starts with * still counts as unrestricted when it is */2 — so 0 0 */2 * 1 only runs on Mondays with an odd day of the month, not "every other day or on Mondays".

Cron runs in the server's time zone — not yours

Cron uses the server's time zone (often UTC), not your local one. A 0 3 * * * is "3 AM server time". Three things follow:

  • Check the server's time zone with date +%Z or timedatectl. cronie also reads CRON_TZ=Europe/Berlin at the top of the crontab (as the tool's General Best Practices block recommends). In containers, set TZ=Europe/Berlin as an environment variable instead — otherwise the container runs in UTC.
  • The Next Scheduled Runs in the builder use your browser's time zone by default. Set your server's zone under Time zone of the server, and the preview shows the runs the way the server will execute them — with the zone abbreviation and relative time. The choice stays saved in your browser.
  • Daylight saving time follows rules of its own — see the next section.

Daylight saving time: jobs between 2 and 3 AM

In time zones with DST there is one night without 02:00–02:59 and one night in which that hour happens twice. cronie treats this differently depending on the job, and the tool calculates exactly the same way:

  • Fixed time (minute and hour without *, e.g. 30 2 * * *): if the time falls into the skipped hour, the job runs right after the jump; in the repeated hour it runs only once.
  • Wildcard jobs (e.g. */15 2 * * * or * * * * *): they follow the wall clock — nothing runs in the skipped hour, and the repeated hour runs twice.

The preview flags affected runs with a yellow note ("DST: …"). To stay clear of these special cases, schedule critical jobs outside 02:00–03:00 or run the server in UTC. A worked example is in the Examples.

Suppressing output: MAILTO vs. /dev/null

By default, cron emails any output (stdout and stderr) of a job to the local system user. A chatty script floods the inbox. There are two levers, which the Reference tab clearly separates:

  • MAILTO="" at the top of the crontab disables emails globally for the whole file.
  • > /dev/null 2>&1 after the command suppresses output per job — and also prevents a process from blocking on a full mail queue.

For web cron jobs, combining both is the most robust. Note: > /dev/null 2>&1 also redirects stderr (2>) — > /dev/null alone would still email error messages.

Use absolute paths

Cron starts with a minimal environment: $PATH is usually just /usr/bin:/bin, and your interactive profile (.bashrc, nvm, rbenv …) is not loaded. A command that works in your shell can fail silently under cron because the binary is not found. So: use absolute paths to the interpreter and script (/usr/bin/php /var/www/html/artisan …) instead of just php …. If a script needs particular environment variables, set them explicitly at the top of the crontab.

Specify the user (in system-wide crontabs)

In /etc/crontab and files under /etc/cron.d/, the username comes before the command — e.g. www-data. This is a common trap: the personal crontab (crontab -e, or crontab -u www-data -e for another user) has no such field (the job runs as that user), while the system-wide files require it. Put www-data in front of the command in a personal crontab anyway, and cron tries to start a program called www-data. The tool's CMS recipes are written for crontab -u www-data -e and therefore come without a user column; for a file under /etc/cron.d/, add the user between schedule and command.

Prevent overlapping runs (flock)

If a job takes longer than its interval (e.g. a */5 import that sometimes runs 7 minutes), cron starts the next run anyway — two instances race and can clash. Guard against it with flock:

*/5 * * * *  flock -n /tmp/mycron.lock php /var/www/html/artisan schedule:run >> /dev/null 2>&1

flock -n only starts the job if the lock file is free, and skips it otherwise. The tool mentions this trick in its General Best Practices block.

Web cron vs. system cron — when to use which

The Web Cron tab builds jobs that call a URL via curl/wget — the schedule still runs in the system cron, only the command is an HTTP request. When is that worth it?

  • Web cron is the right choice on shared hosting, where you lack shell access or control over CLI tools, and for CMS endpoints triggered over HTTP anyway (wp-cron.php, Drupal's /cron/KEY).
  • System cron with CLI (WP-CLI, Drush, artisan, console) is faster, more robust and bypasses the web server timeout. Where you have shell access it is the better choice — the CMS & Frameworks tab recommends the CLI path throughout.

When you protect a web cron endpoint, remember the Web Cron Tips from the tool: discard the response with -o /dev/null and add -sS (quiet, but real errors stay visible), send output to a log file, secure the URL with a token/IP restriction/basic auth, set a timeout, and use HTTPS exclusively so credentials are not sent in plain text. The generator also always adds curl --fail: if the endpoint answers with a 4xx or 5xx, curl exits with an error code instead of quietly counting the failure as a success.

For basic auth, keep in mind: a password on the command line sits in the crontab and, while the job runs, is visible to other users in the process list (ps). Tick Read credentials from ~/.netrc in the tool and enter an HTTP Auth User, and the line only contains --netrc, while the tool hands you the matching .netrc line to protect with chmod 600. Without a user, the checkbox has no effect: curl then gets no auth option at all, and because of --fail, every run fails with HTTP 401. You can leave the password empty and add it to .netrc yourself.

The percent sign ends the command

A crontab quirk that catches even seasoned admins: an unescaped % ends the command, and everything after it is passed to the command as input. A date +%Y-%m-%d in a backup's file name or a URL containing %20 breaks silently this way. In a crontab, write every % as \%.

The Web Cron generator takes care of this automatically: the Generated Crontab Line contains \%, while Command only holds the unescaped version you can test straight in a shell. When you copy a command from the shell into the crontab by hand, it is up to you to remember.

Steps across the hour: */7 is not every 7 minutes

A step restarts at 0 every hour (or every day). */7 in the minute field fires at minutes 0, 7, … 56 — and then at minute 0 again. Between 56 and 0 there are only four minutes. Likewise, 0 */5 * * * runs at 0, 5, 10, 15 and 20 o'clock and then again at midnight. The builder warns about such steps and names the actual gap. For even spacing, pick a step that divides 60 (minute) or 24 (hour) evenly.

The short form 5/15 is just as tricky: cronie reads it as 5-59/15, other cron variants reject it. The tool accepts it with a warning — to be safe, write the spelled-out form.

Quartz characters do not work in Unix cron

The characters ?, L, W and # as well as a seconds field belong to Quartz, Spring and other schedulers, not to plain Unix cron. A 0 0 L * * ("last day of the month") or 5#3 ("3rd Friday") is not understood by Vixie cron or cronie. The Cron Job Helper therefore rejects such expressions as errors, and it answers a sixth time field with a warning, because cron would read it as the command. For a classic crontab, stick to * , - /.

You can still get the last day of the month — with the workaround from the Reference tab: the job starts on days 28–31 and checks for itself whether tomorrow is the 1st.

0 0 28-31 * * [ "$(date -d tomorrow +\%d)" = 01 ] && command

Containers: log to stdout

In Docker/Podman, write cron output to /proc/1/fd/1 (or stdout) so it appears in docker logs — otherwise it vanishes. And mind the time zone: without TZ, the container runs in UTC. Both are in the tool's Container Cron Tips.

Combine with other JPKCom tools

  • Docker tools — when you turn the container patterns from the Docker / DDEV tab into a real setup.
  • Time & timestamp tools — to clarify the server time zone and Unix timestamps before committing to a clock time.

More context: the overview for the big picture, the Manual for every tab, and the Examples for concrete expressions. Try it all directly in the tool.