Linux Crontab in Practice: Commands, Logs, and Debugging
crontab is a program, not just a syntax
Most tutorials present crontab as "a five-field expression", but the first time a job silently fails on a production box you learn the truth: the expression is the easy half. crontab is the management entry point for the cron daemon — it decides whether your job runs, as whom, and where the failure evidence lands.
A full crontab entry looks like this:
30 3 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1
The first five fields are the schedule rule (minute hour day month weekday); the rest is the command itself. For writing rules, point-and-click with the online Cron expression generator and reverse-check the human-readable meaning with the parser — this post covers the other half: installing, listing, and debugging.
Three commands you must know, and one dangerous button
crontab -e # edit the current user's jobs
crontab -l # list the current user's jobs
crontab -r # remove ALL jobs of the current user (no confirmation!)
-r sits right next to -e on the keyboard; a slipped finger silently wipes every job — a classic ops incident. Two defenses:
- Back up before editing:
crontab -l > ~/crontab.bak.$(date +%F) - To retire one job, always comment the line out via
crontab -e(leading#), never rebuild with-r.
Saving from crontab -e hot-reloads on most distributions — no service restart needed. Editing the files under /var/spool/cron/ directly does not trigger a reload, so never bypass the crontab command.
User crontab vs /etc/crontab vs /etc/cron.d
| Location | Format | Best for |
|---|---|---|
| crontab -e (/var/spool/cron/) | 5 fields, runs as the owning user | personal / app-account jobs |
| /etc/crontab | 6 fields — extra "user" field | the traditional system-wide file |
| /etc/cron.d/xxx | same 6 fields as /etc/crontab | recommended for packages and ops scripts — one file per job group |
File names under /etc/cron.d/ may only contain letters, digits, underscores, and hyphens — a dotted name like backup.sh is silently ignored by cron, the number-one cause of "the file is there but nothing runs". The file must also be mode 0644 and owned by root, or it is rejected.
@reboot and friends: seven shortcuts
@reboot /opt/scripts/init-cache.sh
@daily /opt/scripts/cleanup.sh
Full list: @reboot (at boot), @yearly/@annually, @monthly, @weekly, @daily/@midnight, @hourly. They work inside /etc/cron.d/ too. @reboot is handy for rebuilding caches, re-mounting, or starting local services after a cloud VM restart — far lighter than a systemd unit.
The job never ran: a five-step debug path
Walk it in order; most cases die in the first three steps:
- Is the cron daemon alive?
systemctl status crond(CentOS) orsystemctl status cron(Debian/Ubuntu). - Read cron's own log:
grep CRON /var/log/cron(CentOS) or/var/log/syslog(Ubuntu). An execution record means scheduling is fine and the script is the suspect; no record at all means the break is at the scheduling layer. - Run the command manually: paste the exact command from the entry into a shell. The cron environment differs from your interactive shell (next step) — this is the root of "works when I run it, not from cron".
- Check PATH: cron jobs typically get only
/usr/bin:/bin. Binaries installed under/usr/local/bin—python3,node,docker— silently vanish. Fix: use absolute paths inside scripts, or declarePATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/binat the top of the crontab. - Check output redirection: without redirection, output goes to local mail (silently dropped on many servers). While debugging, always append
>> /path/log 2>&1.
The timezone trap: cron follows the system clock
0 3 * * * on a TZ=UTC cloud instance fires at 3 AM UTC — 11 AM in Beijing, not 3 AM local. Three checks:
timedatectl # current server timezone
date # what the server thinks "now" is
ls -l /etc/localtime # where the timezone link points
For distributed teams, convert once with a timezone converter before writing the entry, and put the agreed interpretation in a comment next to the job. Docker containers default to UTC and usually ship without a cron daemon — schedule container jobs from the host via docker exec, or move to systemd timers / an in-app scheduler.
Second-level scheduling: cron cannot, do not force it
Standard cron's finest grain is one minute. The folk remedy for every 30 seconds (duplicate the line, add sleep 30) works until the job itself takes longer than 30 seconds and runs stack up. When you truly need seconds:
- Loop + sleep:
while true; do ...; sleep 30; doneinside the script, supervised by systemd or supervisor — the most transparent option. - systemd timer:
OnUnitActiveSec=30sgives native second-level scheduling plus journald logs and dependency management.
One more habit worth forming: before shipping a schedule, translate it back to natural language with the expression parser. It intercepts an entire class of "I thought it ran daily, it actually ran on the 3rd of each month" incidents.
Cheat sheet
| Requirement | Expression |
|---|---|
| Daily at 3:30 AM | 30 3 * * * |
| Mondays at 9:00 AM | 0 9 * * 1 |
| 1st of the month, midnight | 0 0 1 * * |
| Every 5 minutes | */5 * * * * |
| Weekdays at 18:00 | 0 18 * * 1-5 |
| At boot | @reboot |
Parse the rule before it ships, back up with -l before editing, and start every investigation in /var/log/cron. Those three sentences dodge nine out of ten crontab pitfalls.
Related Tools
Related Articles
docker run to docker-compose: The Complete Migration Guide (with Flag Map)
Migrate docker run commands to docker-compose.yml: five advantages of compose over raw run, a flag-by-flag mapping table (ports, volumes, env, restart, networks), the classic pitfalls (bare -e passthrough, anonymous volumes, --rm semantics), and an online converter that runs locally in your browser.
Permission denied (publickey): 6 Reasons Git Push Fails Over SSH (and the Fix for Each)
git clone or push rejected with Permission denied (publickey) fatal: Could not read from remote repository? Covers missing keygen, key not loaded in the agent, public key not added to GitHub/GitLab, multi-account key routing with ~/.ssh/config, deploy key limits, and wrong remote URLs — with ssh -v diagnostics.
npm ERR! ERESOLVE: peer dependency conflicts — the complete fix guide
npm install fails with ERESOLVE peer dependency conflicts? Understand what peer dependencies are, why npm 7+ enforces them, and 4 resolution strategies (force, legacy-peer-deps, overrides, manual) with trade-offs.