cron is the traditional Linux scheduler for recurring tasks. It runs commands or scripts automatically at specific times, which makes it useful for backups, cleanup jobs, report generation, and health checks.
If you have ever needed something to run every night at 2:00 AM or every five minutes, cron is one of the first tools to learn.
Each user can have a personal crontab file.
Open it for editing:
crontab -eList current entries:
crontab -lRemove the current user crontab:
crontab -rBe careful with crontab -r because it deletes all scheduled entries for that user.
Tip: Always run
crontab -lbefore making major changes so you know exactly what is already scheduled.
A cron schedule has five time fields followed by the command:
* * * * * /path/to/commandThe fields are:
Examples:
0 2 * * * /opt/scripts/backup.sh
*/5 * * * * /opt/scripts/health-check.sh
30 6 * * 1 /opt/scripts/weekly-report.shThese mean:
0 2 * * * = every day at 02:00*/5 * * * * = every five minutes30 6 * * 1 = every Monday at 06:30Run a backup every night:
0 1 * * * /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1This appends both standard output and errors to a log file.
15 3 * * 0 /usr/local/bin/archive-old-logs.sh*/10 * * * * curl -fsS http://localhost:8080/health || echo "health check failed"A health check job can test an internal endpoint and alert if the result is unexpected.
Note: Cron runs with a limited environment. Commands that work in your interactive shell may fail unless you use full paths or set needed variables explicitly.
Not all scheduled jobs live in a user crontab. Linux systems also include system-wide scheduling locations.
/etc/cron.d/This directory contains cron files managed by packages or administrators. Files here usually include an extra field for the user account that should run the job.
Example entry:
0 4 * * * root /usr/local/bin/system-backup.sh/etc/cron.daily/, /etc/cron.weekly/, and friendsThese directories hold scripts that run on a daily, weekly, or monthly schedule depending on the system configuration.
You may see:
/etc/cron.hourly//etc/cron.daily//etc/cron.weekly//etc/cron.monthly/Dropping a script into the right directory can be simpler than writing a full cron expression for routine maintenance tasks.
When writing cron jobs:
/usr/bin/python3A script that depends on your interactive PATH, aliases, or current directory is likely to break under cron.
Many modern Linux systems use systemd timers as a more structured alternative to cron. Timers pair with systemd service units and often provide clearer logging, dependency handling, and status inspection.
A timer based workflow might look like this:
systemctl list-timers
sudo systemctl enable --now backup.timer
journalctl -u backup.serviceCron is still widely used and worth learning, but systemd timers are often preferred for services that already live in the systemd ecosystem.
If a cron job did not run, check:
A job that silently fails is often missing environment variables, permissions, or full command paths.
Cron is a lightweight and powerful scheduler built around time expressions and shell commands. Use crontab -e to edit, crontab -l to inspect, and system cron directories for broader maintenance tasks.
For modern service-based workflows, systemd timers can offer better observability, but cron remains a core Linux skill.
<MCQExercise
exercise={{
id: "linux-cron-jobs-q1",
title: "Exercise 1: Reading a cron expression",
description: "What does the schedule */5 * * * * mean?",
options: [
{ id: "a", text: "Run once every five hours" },
{ id: "b", text: "Run every five minutes" },
{ id: "c", text: "Run every day at 05:00" },
{ id: "d", text: "Run every fifth day of the month" },
],
correctId: "b",
explanation: "The first field is minutes, and */5 means every five minutes. The remaining wildcard fields mean any hour, day, month, and weekday.",
wrongExplanations: {
a: "The expression does not operate on hours. The */5 is in the minute field.",
c: "A daily 05:00 schedule would look different, such as 0 5 * * *.",
d: "Day-of-month is the third field, not the first field.",
},
}}
/>
<MCQExercise
exercise={{
id: "linux-cron-jobs-q2",
title: "Exercise 2: Editing scheduled tasks",
description: "Which command opens the current user's crontab for editing?",
options: [
{ id: "a", text: "cron -edit" },
{ id: "b", text: "crontab -e" },
{ id: "c", text: "systemctl edit cron" },
{ id: "d", text: "journalctl -u cron" },
],
correctId: "b",
explanation: "crontab -e opens the current user's scheduled task list in an editor. It is the standard way to add or modify user-level cron entries.",
wrongExplanations: {
a: "There is no standard cron -edit command for user crontabs.",
c: "systemctl edit is for systemd unit overrides, not crontab entries.",
d: "journalctl reads logs and does not edit schedules.",
},
}}
/>
<MCQExercise
exercise={{
id: "linux-cron-jobs-q3",
title: "Exercise 3: Limited cron environment",
description: "Why do many cron jobs use absolute command paths like /usr/bin/python3?",
options: [
{ id: "a", text: "Because cron cannot run scripts unless the command starts with /usr" },
{ id: "b", text: "Because cron deletes the PATH variable completely on all systems" },
{ id: "c", text: "Because cron often runs with a limited environment, so explicit paths reduce failures" },
{ id: "d", text: "Because absolute paths automatically rotate logs" },
],
correctId: "c",
explanation: "Cron jobs often run with a minimal environment compared to your interactive shell. Using full paths makes the job more predictable and avoids command lookup surprises.",
wrongExplanations: {
a: "Cron can run commands from other paths too, but explicit paths are safer.",
b: "Cron environments are limited, but it is not accurate to say PATH is always completely deleted.",
d: "Log rotation is a separate function and is not caused by absolute paths.",
},
}}
/>
Almost always PATH. Cron runs with a minimal environment, typically just /usr/bin:/bin, so a command that resolves in your interactive shell is not found by cron, and your shell's profile is never sourced. Use absolute paths for everything (/usr/bin/python3 /opt/app/job.py), or set PATH= at the top of the crontab. The second most common cause is a relative path in the script itself, since cron starts in the user's home directory rather than where the script lives.
By default cron emails output to the account's local mailbox, which on most servers nobody reads. Redirect it explicitly instead: 0 3 * * * /opt/backup.sh >> /var/log/backup.log 2>&1. The 2>&1 is essential, without it you capture normal output and lose the error messages, which are exactly what you need. To confirm cron even ran, check journalctl -u cron on Debian and Ubuntu or journalctl -u crond on RHEL-family systems, or /var/log/syslog.
crontab -e edits the current user's personal crontab, stored under /var/spool/cron, and its entries run as that user with five time fields. /etc/crontab and files in /etc/cron.d/ are system-wide and take an extra field for the user to run as. Six fields before the command. Forgetting that extra field is the usual reason an /etc/cron.d entry silently does nothing. Prefer a file in /etc/cron.d/ for anything managed by configuration management, since it can be deployed as a file.
Cron for simple, portable, recurring jobs. It is universally available and a one-line entry. systemd timers for anything that needs more: journalctl integration for logs, Persistent=true to run a missed job after downtime, dependency ordering, resource limits, and randomised delays to avoid thundering herds. Timers need two files rather than one line, which is the main cost. In containers, neither may be present. Most base images run no cron daemon at all, so scheduling belongs outside the container.
*What does /5 mean in a cron expression?
A step value: "every 5" within that field. In the minute position, */5 * * * * runs at minute 0, 5, 10 and so on. In the hour position, 0 */6 * * * runs at 00:00, 06:00, 12:00 and 18:00. Steps are not modulo arithmetic across boundaries, so */7 in the minute field fires at 0, 7, â¦, 56 and then again at 0. A 4-minute gap at the top of each hour. For intervals that do not divide 60, list the values explicitly.
Set up SSH keys properly: generate Ed25519 keys with ssh-keygen, fix permission errors that block a valid key, use ssh-agent, ~/.ssh/config and ProxyJump.
Set up a Linux firewall without locking yourself out: compare ufw, firewalld and iptables, allow SSH first, default deny, zones and persistent rules.
Find what went wrong in Linux logs: /var/log versus the systemd journal, tail -F over -f, grep filtering, logrotate, journald size caps and error triage.