cron Cheatsheet

at and Common Recipes

Use this cron reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

at, for a job that runs once

cron repeats. at queues a command to run one time at a given moment and then forgets it. The command is read from stdin, and the environment plus the working directory are captured at submit time.

at 22:00 <<'EOF'
/srv/app/bin/deploy.sh --tag v2.1.0
EOF

echo '/usr/bin/systemctl restart nginx' | at now + 5 minutes
at -f /srv/app/bin/migrate.sh 03:00 tomorrow
at -t 202609160300 -f /srv/app/bin/migrate.sh
CommandDoes
at TIMEread commands from stdin, queue them for TIME
at -f FILE TIMEqueue a script file instead of stdin
at -t YYYYMMDDhhmmgive the time in a fixed numeric format
at -q Qput the job in queue letter Q, a to z
at -mmail the owner even when there is no output
at -Mnever mail
atq (at -l)list your pending jobs, root sees everyone's
atrm N (at -d N)delete job number N
at -c Nprint job N, including the whole captured environment
batch (at -b)run as soon as load average drops below 1.5

Time expressions are generous. All of these parse:

at 16:20
at noon
at midnight
at teatime            # 16:00
at now + 30 minutes
at now + 3 days
at 10:00 Jul 31
at 2027-01-01 09:00
at 4pm + 2 days
at tomorrow
at next week

atq output is jobnumber date hour queue owner. Higher queue letters run at a higher nice value, and batch is queue b.

- The atd daemon must be running or nothing fires. Check with systemctl status atd (atd on Debian, atd from the at package on Fedora and Arch). - Jobs live in /var/spool/cron/atjobs (Debian) or /var/spool/at, and they survive a reboot. A job whose time passed while the machine was off runs at the next atd start. - Access is controlled by /etc/at.allow and /etc/at.deny, with the same precedence rule as cron: if at.allow exists, only the users listed in it may submit, and at.deny is ignored. - at captures the environment, so at -c is the fastest way to see exactly what a queued job will run with. This is one of the few places where the environment is not stripped the way cron strips it. - On macOS atd ships disabled. sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist enables it, and on a recent macOS that path is protected, so a launchd job or a timer is the better answer there.

Preventing overlapping runs with flock

A five minute job that sometimes takes seven minutes will eventually run twice at once, corrupt something, and be very hard to diagnose. Wrap it.

*/5 * * * * /usr/bin/flock -n /tmp/sync.lock \
  /srv/app/bin/sync.sh >> /var/log/sync.log 2>&1
FlagBehavior when the lock is held
-nexit immediately with status 1, skip this run
-w 30wait up to 30 seconds, then give up
(neither)wait forever, so runs pile up
-E 0exit 0 instead of 1 when -n or -w gives up, so cron stays quiet

-n is almost always what you want, with -E 0 added once you are sure a skipped run is not worth an alert. The lock file is just a path flock can open, and it is never deleted, which is correct. Do not put it somewhere systemd-tmpfiles will clean mid-run, use /var/lock or /run/lock on a long-lived box.

The self-locking form, useful inside a script that anyone might run by hand:

#!/bin/bash
set -euo pipefail
exec 9> /var/lock/sync.lock
flock -n 9 || { echo "already running"; exit 0; }
# real work below, lock is released when the script exits

Recipes

Every line below is a complete crontab entry. Copy, adjust the path, and remember that % must be escaped as \% in a crontab and that the whole line runs under /bin/sh with a bare PATH.

ScheduleLine
Every 5 minutes*/5 * * * * /srv/app/bin/sync.sh
Weekdays at 09:000 9 * * 1-5 /srv/app/bin/report.sh
First of the month at 04:000 4 1 * * /srv/app/bin/invoice.sh
Every 15 min, business hours*/15 9-17 * * 1-5 /srv/app/bin/poll.sh
Locked, no overlap*/5 * * * * flock -n /var/lock/s.lock /srv/app/bin/sync.sh
Every hour on the half hour30 * * * * /srv/app/bin/rotate.sh
Twice a day0 6,18 * * * /srv/app/bin/refresh.sh
Every 2 hours0 */2 * * * /srv/app/bin/check.sh
Every Sunday at 03:1515 3 * * 0 /srv/app/bin/weekly.sh
Last minute of the year59 23 31 12 * /srv/app/bin/yearend.sh
At boot@reboot /srv/app/bin/warm-cache.sh
With a dated log file0 2 * * * /srv/app/bin/b.sh >> /var/log/b-$(date +\%F).log 2>&1

The same set, written out with logging and locking the way it would actually ship:

SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=ops@example.com

# Every 5 minutes, skipped rather than doubled up.
*/5 * * * * flock -n /var/lock/sync.lock \
  /srv/app/bin/sync.sh >> /var/log/app/sync.log 2>&1

# Weekdays at 09:00, mail only on failure.
0 9 * * 1-5 /srv/app/bin/report.sh > /dev/null

# First of the month at 04:00, day-of-week left as * on purpose.
0 4 1 * * /srv/app/bin/invoice.sh >> /var/log/app/invoice.log 2>&1

# Every 15 minutes during business hours, weekdays only.
*/15 9-17 * * 1-5 /srv/app/bin/poll.sh >> /var/log/app/poll.log 2>&1

# Nightly, with a timestamped log. The percent must be escaped.
0 2 * * * /srv/app/bin/backup.sh \
  >> /var/log/app/backup-$(date +\%Y\%m\%d).log 2>&1

Recipes that look right and are not

WrittenActually meansWanted
*/5 9-17 * * * on a job taking 8 minoverlapping runs foreveradd flock -n
0 0 1 * 1the 1st or any Monday0 0 1 * *
*/35 * * * *:00 and :35, then a 25 min gap0,35 * * * * if that is truly wanted
0 9 * * 7Sunday, only where 7 is accepted0 9 * * 0
@daily for a report at 09:00midnight0 9 * * *
0 2 * * * date +%Ftruncated at the %date +\%F
*/10 * * * * ./run.shrelative to $HOME, usually not foundan absolute path
0 3 * * * python app.pywrong interpreter or none found/usr/bin/python3 /srv/app/app.py

Choosing the tool

NeedUse
Repeating job on any Unixcrontab
Repeating job, needs logs and limitssystemd timer
One shot, at a specific timeat
One shot, whenever the box is idlebatch
Must catch up after downtimeanacron, or Persistent=true
Must never overlapflock -n, or a systemd service
Sub-minute intervala systemd timer with OnUnitActiveSec=, or a daemon

Verify anything on this page against crontab(5) and at(1) on the box you are targeting, because cron implementations differ in exactly the places that bite.