Sysadmin

Linux Cron Jobs: A Complete Guide from Syntax to Debugging

Linux Cron Jobs: A Complete Guide from Syntax to Debugging

Monday morning. I open the infrastructure monitoring dashboard for a healthcare client. The last successful backup dates back to Friday evening. The nightly task tried to run all weekend, but no data was saved. The backup script worked perfectly when I ran it manually on Friday afternoon. The difference? I executed it from my shell, not via cron. The lack of proper environment variables caused the database connection to fail silently.

If you administer Linux servers, cron jobs are the beating heart of automated operations: backups, log rotation, security checks, config syncs. Unfortunately, the cron execution environment is deliberately minimal, which generates obscure issues. In this guide, I break down the syntax, the differences between user and system cron, and debugging techniques to figure out exactly why a scheduled task stops working.

What is a cron job and when to use it

A cron job is a scheduled task managed by the cron daemon (crond on Red Hat-based distributions). The daemon wakes up every minute, reads the configuration files, and launches commands whose schedule matches the current time. Use cron jobs for anything that requires repetitive and predictable execution: nightly backups, Let’s Encrypt certificate renewals, temporary directory cleanups, and sending reports.

Don’t use cron for tasks that require complex dependency management or that must guarantee execution even if the server was powered off at the designated time. For these scenarios, systemd timers offer more advanced features.

Crontab syntax explained field by field

Every line in a crontab file contains six fields. The first five define the frequency, and the sixth specifies the command to execute.

# Sintassi: minuto ora giorno mese giorno_settimana comando
0 2 * * * /backup/script.sh

Fields accept integers, asterisks (meaning “every value”), comma-separated lists (1,15), ranges (1-5), and the step operator (*/15 to indicate “every 15 units”). The weekday field uses values from 0 to 7, where both 0 and 7 represent Sunday.

Practical scheduling examples (every minute, hour, day, week)

Let’s look at the most requested production configurations. To run a health check every 15 minutes, you use the step operator in the minute field. For a Monday morning report, you specify the day of the week.

*/15 * * * * /check/health.sh
0 9 * * 1 /weekly/report.sh

If you need an hourly cron configuration, the correct syntax requires a specific minute and an asterisk for the hour. For example, to run a script at minute 0 of every hour:

0 * * * * /opt/scripts/hourly_check.sh

To execute a command every minute—useful during initial testing—you use five asterisks.

Editing the crontab with crontab -e

To edit your user’s cron table, the standard command is crontab -e. This opens the configuration file in the default editor (usually vi or nano). If it’s your first time running it, the system will ask you to choose the default editor.

crontab -e

To view active rules without opening the editor, use crontab -l. I recommend always backing up your configuration before modifying it by redirecting the output to a file.

crontab -l > ~/crontab_backup_$(date +%F).txt

System cron vs user cron (/etc/cron.d, /etc/crontab)

There is a fundamental difference between a user crontab and a system crontab. The user crontab, managed via the crontab command, doesn’t specify the execution user because it runs with the privileges of the user who created it.

Files in /etc/cron.d/ and the /etc/crontab file belong to the system and contain a sixth field before the command: the user who should execute the task.

# Esempio in /etc/crontab
0 3 * * * root /usr/local/bin/maintenance.sh

In enterprise environments, I prefer using /etc/cron.d/ for system tasks. It allows you to distribute individual files via Ansible or provisioning scripts without touching the global crontab, reducing the risk of overwrites.

Environment variables in cron jobs (PATH, MAILTO)

Here lies the issue that has cost me the most hours in production. Cron does not load the user profile. It doesn’t execute .bashrc, .profile, or similar files. The environment is stripped down to the bare minimum. The PATH variable is often limited to /usr/bin:/bin.

If your script uses commands located in /usr/sbin/ (like iptables) or /usr/local/bin/ (like custom commands or Python pip), the cron job will fail with a silent “command not found”.

Always set the PATH at the top of the crontab file:

PATH=/usr/local/bin:/usr/bin:/bin

The MAILTO variable defines where to send the command’s output. By default, cron sends an email to the user who owns the cron job. If the server doesn’t have a configured MTA, the output is lost. Set MAILTO=”” to disable sending, but make sure to redirect the output to a log file.

Redirecting cron output and logs

A cron job executed without redirection sends all output (both standard output and standard error) via email. On most modern servers, this email ends up in /var/mail/$USER or gets discarded. To track what happens, you must capture the logs to a file.

* * * * * /script.sh >> /var/log/myscript.log 2>&1

The 2>&1 construct redirects standard error (file descriptor 2) to the same location as standard output (file descriptor 1). The double greater-than sign (>>) appends data to the file without overwriting it. If you use a single greater-than sign (>), you erase the previous log on every execution, losing the history.

Verifying if a cron job ran

Don’t just trust that the script produced the expected effect. Check the system log to confirm that the cron daemon actually launched the process. On Debian/Ubuntu distributions, cron logs actions to /var/log/syslog. On RHEL/CentOS, use /var/log/cron.

Search for the string CMD to see executed commands, or the string CRON for generic daemon messages.

Common errors — missing PATH, permissions, non-executable scripts

Debugging a “cron not working” issue almost always traces back to one of these four errors.

The first is an incomplete PATH, as we’ve seen. Use absolute paths for every single binary in your script.

The second involves permissions. If you create a script and run it with bash script.sh it works, but if you call it directly in cron (/script.sh) it requires the execute bit. Run chmod +x /script.sh.

The third error concerns Windows-style line endings (CRLF). If you edit a script on a Windows machine and upload it to Linux, cron cannot correctly interpret the shebang (#!/bin/bash). The result is a “bad interpreter” error. Convert the file with dos2unix or set the LF format in your editor.

The fourth is the missing final newline. The last line of the crontab file must end with a newline character. Without it, cron might ignore the last rule.

Debugging with syslog and journalctl

When a task doesn’t start and you have no application logs, you must inspect the system logs. On systems with traditional syslog, filter the daemon messages.

grep CRON /var/log/syslog

If your server uses systemd, the cron service logs events to the journal. You can query it specifically for the cron unit.

journalctl -u cron

Add the -f option to follow the logs in real time while waiting for the scheduled task, and –since “1 hour ago” to limit the output.

[Read also: Finding and freeing up disk space in Linux]

Modern alternative: systemd timers

For complex new implementations, I always evaluate systemd timers instead of cron. Timers offer concrete advantages in critical infrastructures. The Persistent=true parameter ensures that a task runs even if the server was powered off at the scheduled time, catching up at the next boot.

Additionally, timers integrate natively with journalctl for logging and allow you to define explicit dependencies (for example, run the backup only after the network mount is active).

The syntax requires two files: a .service to define the command and a .timer to define the schedule. You can check their status with systemctl list-timers.

For simple, straightforward tasks, cron remains the fastest tool. For architectures with hundreds of VMs where reliability and traceability are mandatory, systemd timers reduce silent incidents.

Official references on configuration and supported formats are available in the crontab documentation on man7.org: https://man7.org/linux/man-pages/man5/crontab.5.html

Share this article:

Written by

Rosario Giordano

Rosario Giordano is a system administrator and IT consultant specializing in cybersecurity and cloud, with over 20 years of experience managing enterprise Linux infrastructures. His areas of expertise include SSH hardening, Kubernetes platforms, PostgreSQL databases, VMware/ Proxmox virtualization, and compliance with NIS2 and ISO 27001 security frameworks