Skip to content

Automation Tools — cron & at

cron lets your scripts run themselves — once you write a script, scheduling it to run every night, every hour, or every minute is a one-liner.

Learning Objectives

  • Schedule recurring jobs with crontab
  • Schedule one-time jobs with at
  • Understand cron's environment and how it differs from your login shell
  • Debug cron jobs that work on the command line but fail when scheduled
  • Manage environment variables in automated scripts

cron Basics

cron is a daemon that executes scheduled commands. Edit your user's schedule with:

crontab -e      # open your crontab for editing
crontab -l      # list current crontab
crontab -r      # remove your crontab (careful — no confirmation!)

Crontab Format

┌───────────── minute (0 - 59)
│ ┌───────────── hour (0 - 23)
│ │ ┌───────────── day of month (1 - 31)
│ │ │ ┌───────────── month (1 - 12)
│ │ │ │ ┌───────────── day of week (0 - 7, 0 and 7 are Sunday)
│ │ │ │ │
* * * * * /path/to/command

Common Cron Expressions

* * * * *               every minute
0 * * * *               every hour (at :00)
0 2 * * *               every day at 2:00 AM
0 2 * * 0               every Sunday at 2:00 AM
0 2 1 * *               first of every month at 2:00 AM
*/15 * * * *            every 15 minutes
0 9-17 * * 1-5          every hour from 9 AM to 5 PM, Monday-Friday

crontab.guru

The site crontab.guru lets you type a cron expression and see a plain-English description. Use it to verify your schedule before deploying.

A Real Crontab

# Daily backup at 2:30 AM
30 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1

# System health check every 5 minutes
*/5 * * * * /home/user/scripts/health_check.sh

# Weekly cleanup every Sunday at midnight
0 0 * * 0 /home/user/scripts/cleanup.sh >> /var/log/cleanup.log 2>&1

The cron Environment Problem

Cron runs with a minimal environment — no ~/.bashrc, no $PATH beyond /usr/bin:/bin. This is the #1 reason cron jobs fail silently.

# Wrong — relies on $PATH
0 2 * * * backup.sh

# Correct — full path
0 2 * * * /home/user/scripts/backup.sh

# Also correct — set PATH in the crontab
PATH=/usr/local/bin:/usr/bin:/bin
0 2 * * * backup.sh

Cron email

By default, cron emails output to the local system user. On most servers there is no local mail, so output disappears. Always redirect output explicitly: command >> /path/to/logfile.log 2>&1.

Debug cron issues by capturing the environment:

# Add this as a test job to see what environment cron uses
* * * * * env > /tmp/cron_env.txt

at — One-Time Scheduled Commands

at 3pm tomorrow                          # opens interactive prompt
at 14:30 2024-01-20                      # specific date and time
echo "/path/to/script.sh" | at 3pm       # non-interactive
echo "rm -f /tmp/tempfile" | at now + 1 hour

atq                                      # list pending jobs
atrm 3                                   # remove job number 3

Systemd Timers (modern alternative)

On systemd-based Linux, timers are more powerful than cron:

systemctl list-timers           # show all active timers
systemctl status cron           # check if cron is running

For production servers, consider systemd timers for better logging and dependency management.


Environment Management

direnv

# .envrc in project directory — loaded automatically when you cd into it
export DATABASE_URL="postgres://localhost/mydb"
export API_KEY="dev-key-here"

dotfiles pattern

# ~/.bash_profile (login shells)
export PATH="$HOME/.local/bin:$PATH"
export EDITOR="vim"

# ~/.bashrc (interactive shells)
alias ll='ls -la'
alias gs='git status'

Practice Exercises

Main (write a short script)

Create ~/scripts/health_check.sh suitable for running via cron every 5 minutes:

#!/usr/bin/env bash
set -euo pipefail

LOGFILE="/var/log/health_check.log"
THRESHOLD_DISK=85
THRESHOLD_MEM=90

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOGFILE"; }

disk_pct=$(df / | awk 'NR==2 {gsub(/%/,"",$5); print $5}')
mem_pct=$(free | awk '/^Mem:/ {printf "%.0f", $3/$2*100}')

if (( disk_pct > THRESHOLD_DISK )); then
    log "ALERT: Disk usage at ${disk_pct}% (threshold: ${THRESHOLD_DISK}%)"
fi

if (( mem_pct > THRESHOLD_MEM )); then
    log "ALERT: Memory usage at ${mem_pct}% (threshold: ${THRESHOLD_MEM}%)"
fi

Schedule it: */5 * * * * /home/user/scripts/health_check.sh

Stretch

  1. Write a cron job that runs every weekday at 9 AM and emails you the current disk usage summary. (Use mail command or redirect to a file if no mail is configured.)
  2. Research the difference between /etc/crontab (system crontab) and user crontabs (crontab -e). What extra field does /etc/crontab have?

Interview Questions

  1. What is the most common reason a cron job fails when the same command works in the terminal?
Show answer

The $PATH environment variable. Cron runs with a minimal $PATH (/usr/bin:/bin), not your login shell's $PATH. Commands that rely on /usr/local/bin, ~/.local/bin, or other custom paths will not be found. The fix: use full absolute paths in cron jobs, or set PATH explicitly at the top of the crontab.

  1. What does */15 mean in the minute field of a crontab?
Show answer

It means "every 15 minutes" — specifically at minutes 0, 15, 30, and 45. The */N syntax means "every N units." So */15 * * * * runs at :00, :15, :30, and :45 past every hour.

  1. How do you prevent two instances of a cron job from running simultaneously?
Show answer

Use a lock file: exec 9>/tmp/myjob.lock; flock -n 9 || exit 0. Or use flock directly: flock -n /tmp/myjob.lock /path/to/script.sh. If a previous instance is still running when cron fires again, the new instance exits immediately rather than running in parallel.


day03-part2-regular-expressions | day04-part2-networking-apis