How to Schedule Tasks with Cron in Bash

How to Schedule Tasks with Cron in Bash

How to Schedule Tasks with Cron in Bash

Cron is the quiet workhorse behind most Linux automation — it’s what runs your backups at 2 a.m., rotates your logs every night, renews your SSL certificates before they expire, and fires off that health-check script every five minutes without anyone having to remember to do it manually. If you write Bash scripts, sooner or later you’ll want one of them to run itself, and cron is almost always the answer.

This guide covers cron syntax, how to write and schedule scripts properly, and the practical details that trip people up in production.

What Cron Actually Is

Cron is a time-based job scheduler built into virtually every Linux distribution, run as a background daemon (crond) that wakes up once a minute, checks a set of schedule files called crontabs, and executes any job whose schedule matches the current time.

Each user can have their own crontab, and there’s also a system-wide crontab and drop-in directories for package-managed jobs.

The Crontab Syntax

A crontab line has six fields:

* * * * * command_to_run
│ │ │ │ │
│ │ │ │ └── day of week (0–7, both 0 and 7 = Sunday)
│ │ │ └──── month (1–12)
│ │ └────── day of month (1–31)
│ └──────── hour (0–23)
└────────── minute (0–59)

Examples:

# Run every day at 2:30 AM
30 2 * * * /usr/local/bin/backup.sh

# Run every 5 minutes
*/5 * * * * /usr/local/bin/check.sh

# Run every Monday at 9:00 AM
0 9 * * 1 /usr/local/bin/weekly_report.sh

# Run on the 1st of every month at midnight
0 0 1 * * /usr/local/bin/monthly_cleanup.sh

# Run every hour during business hours (9 AM–5 PM), weekdays only
0 9-17 * * 1-5 /usr/local/bin/hourly_check.sh

# Run twice a day, at 6 AM and 6 PM
0 6,18 * * * /usr/local/bin/sync.sh

Special characters: * means “every value,” , separates a list of values, - defines a range, and / defines a step (e.g., */5 means “every 5 units”).

Editing Your Crontab

# Edit your personal crontab
crontab -e

# List your current crontab entries
crontab -l

# Remove your entire crontab
crontab -r

# Edit another user's crontab (requires root)
sudo crontab -u username -e

crontab -e opens the crontab in your default editor (set via the EDITOR environment variable) and validates syntax on save — if there’s a syntax error, it will warn you before committing the change.

Special Time Strings

Cron supports several human-readable shortcuts:

@reboot     /path/to/script.sh     # run once at system startup
@yearly     /path/to/script.sh     # equivalent to "0 0 1 1 *"
@monthly    /path/to/script.sh     # equivalent to "0 0 1 * *"
@weekly     /path/to/script.sh     # equivalent to "0 0 * * 0"
@daily      /path/to/script.sh     # equivalent to "0 0 * * *"
@hourly     /path/to/script.sh     # equivalent to "0 * * * *"

@reboot is particularly useful for starting a service or script automatically after a server restart.

Writing Cron-Safe Bash Scripts

Cron runs jobs with a minimal environment — a bare PATH, no shell aliases, no interactive configuration files sourced. Scripts that work perfectly when run manually often fail silently under cron because of this. A few practices fix nearly every cron-related issue:

#!/bin/bash
# cron-safe script template

# Use absolute paths for everything
SCRIPT_DIR="/usr/local/bin"
LOG_FILE="/var/log/mytask.log"

# Explicitly set PATH if the script relies on tools cron might not find
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

{
    echo "=== Run started: $(date) ==="
    /usr/local/bin/actual_task.sh
    echo "=== Run finished: $(date) ==="
} >> "$LOG_FILE" 2>&1

How this works internally: wrapping the commands in { ... } >> "$LOG_FILE" 2>&1 redirects both standard output and standard error from every command inside the block to the log file, giving you a complete record of what happened — critical because cron jobs run unattended and their output is easy to lose otherwise.

Redirecting Output and Handling Errors

By default, cron emails any output a job produces to the crontab owner (if mail is configured on the system). In practice, most administrators redirect output to a log file instead and, optionally, alert only on failure:

*/10 * * * * /usr/local/bin/check_disk.sh >> /var/log/check_disk.log 2>&1

To completely suppress output (not recommended for critical jobs, but sometimes useful for noisy ones):

*/10 * * * * /usr/local/bin/noisy_script.sh > /dev/null 2>&1

Alerting Only on Failure

#!/bin/bash

OUTPUT=$(/usr/local/bin/backup.sh 2>&1)
if [[ $? -ne 0 ]]; then
    echo "$OUTPUT" | mail -s "Backup FAILED on $(hostname)" admin@example.com
fi

Preventing Overlapping Runs

If a job might occasionally run longer than its scheduled interval, you risk multiple overlapping instances stacking up. flock is the standard solution:

*/5 * * * * /usr/bin/flock -n /tmp/mytask.lock /usr/local/bin/mytask.sh

flock -n tries to acquire an exclusive lock on the given lock file; if another instance already holds it, the new invocation exits immediately instead of running concurrently.

System-Wide Cron Locations

Beyond per-user crontabs, cron jobs can also be defined in:

# Example /etc/cron.d/myapp entry — note the extra "user" field
0 3 * * * appuser /usr/local/bin/myapp_backup.sh

Real-World Automation Examples

Nightly Database Backup with Rotation

#!/bin/bash
# /usr/local/bin/db_backup.sh

BACKUP_DIR="/backups/db"
DATE=$(date +%Y%m%d)
RETENTION_DAYS=14

mkdir -p "$BACKUP_DIR"
mysqldump -u backup_user -p"$DB_PASSWORD" mydb | gzip > "$BACKUP_DIR/mydb_$DATE.sql.gz"

# Delete backups older than retention period
find "$BACKUP_DIR" -name "*.sql.gz" -mtime +$RETENTION_DAYS -delete
0 2 * * * /usr/local/bin/db_backup.sh >> /var/log/db_backup.log 2>&1

Log Rotation and Cleanup

0 0 * * * find /var/log/myapp -name "*.log" -mtime +30 -delete

Certificate Renewal Check

0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"

Periodic Health Check with Alerting

#!/bin/bash

if ! curl -sf https://myapp.example.com/health > /dev/null; then
    echo "Health check failed at $(date)" | mail -s "ALERT: myapp down" admin@example.com
fi
*/2 * * * * /usr/local/bin/health_check.sh

Best Practices

Security Considerations

Optimization Tips

Troubleshooting Common Issues

Problem: The job works when run manually but not under cron. This is almost always an environment difference — missing PATH entries, unset environment variables, or reliance on an interactive shell’s config files. Test with a stripped-down environment, and use absolute paths everywhere.

Problem: No output or errors are visible when a job fails. Redirect both stdout and stderr explicitly to a log file (>> logfile 2>&1); by default, any output not redirected is only mailed to the user, and if local mail isn’t configured, it’s silently lost.

Problem: A job runs twice at the scheduled time. Check for duplicate entries across multiple crontabs (crontab -l for the user, plus /etc/cron.d/, plus /etc/crontab) — it’s easy to accidentally define the same job in two places.

Problem: Cron seems to not be running at all. Verify the cron daemon itself is running (systemctl status cron or crond, depending on distribution) and check the system log (/var/log/syslog or /var/log/cron) for cron’s own execution records.

Common Mistakes

  1. Relying on relative paths or a PATH that only exists in an interactive shell.
  2. Not redirecting output, leading to silently lost error messages.
  3. Scheduling overlapping jobs without a locking mechanism like flock.
  4. Running everything as root out of convenience rather than using a scoped service account.
  5. Forgetting that day-of-week and day-of-month are combined with OR logic (not AND) when both are set to non-* values, which can cause a job to run more often than expected.

Frequently Asked Questions

What’s the difference between crontab -e and editing /etc/crontab directly? crontab -e edits a per-user crontab and doesn’t require specifying a user field. /etc/crontab and files in /etc/cron.d/ are system-wide and require an explicit user field indicating who the job runs as.

How do I run a cron job every 15 minutes? Use a step value: */15 * * * * command runs at minutes 0, 15, 30, and 45 of every hour.

Why didn’t my cron job send me an email on failure? Cron’s built-in mail behavior depends on a local mail transfer agent (like sendmail or postfix) being configured on the system. Many modern servers don’t have one set up, so output silently goes nowhere unless you redirect it to a file or handle alerting explicitly in the script.

Is there a modern alternative to cron? systemd timers offer more advanced scheduling, dependency management, and logging integration via journalctl, and are increasingly used on systemd-based distributions as a cron alternative, though cron remains simpler and more universally available.

Summary

Cron turns any Bash script into something that runs itself, reliably, on a schedule you define with a compact but powerful syntax. The real skill isn’t memorizing the five schedule fields — it’s writing scripts that behave correctly in cron’s minimal environment, logging their output somewhere useful, and protecting against overlapping runs. Get those fundamentals right, and cron becomes one of the most dependable pieces of infrastructure on any Linux system.

References

Exit mobile version