Portal | Level: L1: Foundations | Topics: Cron & Job Scheduling, Bash / Shell Scripting, systemd | Domain: Linux
Cron & Job Scheduling - Primer¶
Why This Matters¶
Scheduled jobs are the backbone of automation. Backups, log rotation, certificate renewal, report generation, database maintenance, cache warming — all cron jobs. They run in the background, silently keeping your infrastructure alive. When they break, things degrade slowly until someone notices the backups have not run in two weeks.
The problem with scheduled jobs is that they fail silently. Nobody watches them. The environment they run in is not what you expect. And when two instances overlap because the first one has not finished, you get corrupted data or deadlocks.
This primer covers classic cron, systemd timers (the modern replacement), and Kubernetes CronJobs — because your jobs need to work everywhere.
Classic Cron¶
Cron Syntax: The Five Fields¶
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or JAN-DEC)
│ │ │ │ ┌───────────── day of week (0-7 or SUN-SAT, 0=7=Sunday)
│ │ │ │ │
* * * * * command
Remember: Cron field order mnemonic: Minute, Hour, Day of month, Month, Day of week — "My Hard Drive Might Die." The first field is always the smallest time unit (minute), increasing left to right.
Common Schedules¶
# Every minute
* * * * * /usr/local/bin/health-check.sh
# Every 5 minutes
*/5 * * * * /usr/local/bin/poll.sh
# Every hour at minute 0
0 * * * * /usr/local/bin/hourly-job.sh
# Every day at 2:30 AM
30 2 * * * /usr/local/bin/backup.sh
# Every Monday at 9 AM
0 9 * * 1 /usr/local/bin/weekly-report.sh
# First day of every month at midnight
0 0 1 * * /usr/local/bin/monthly-cleanup.sh
# Every weekday at 6 PM
0 18 * * 1-5 /usr/local/bin/eod-report.sh
# Every 15 minutes between 8 AM and 6 PM
*/15 8-18 * * * /usr/local/bin/business-hours-check.sh
# Twice a day (6 AM and 6 PM)
0 6,18 * * * /usr/local/bin/sync.sh
Special Strings¶
@reboot Run once at startup
@hourly 0 * * * *
@daily 0 0 * * *
@weekly 0 0 * * 0
@monthly 0 0 1 * *
@yearly 0 0 1 1 *
Managing Crontabs¶
User Crontabs¶
# Edit current user's crontab
crontab -e
# List current user's crontab
crontab -l
# Edit another user's crontab (as root)
crontab -e -u deploy
# Remove current user's crontab (DANGEROUS — no confirmation)
crontab -r
# Remove with confirmation (install this habit)
crontab -ri
User crontabs are stored in /var/spool/cron/crontabs/ (Debian) or /var/spool/cron/ (RHEL). Do not edit these files directly.
System Crontabs¶
# System crontab (has an extra USER field)
cat /etc/crontab
# Drop-in directory (one file per job)
ls /etc/cron.d/
# Periodic directories (scripts, not cron syntax)
ls /etc/cron.hourly/
ls /etc/cron.daily/
ls /etc/cron.weekly/
ls /etc/cron.monthly/
/etc/cron.d/ Format¶
# /etc/cron.d/backup
# Note: includes username field (6th field)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com
30 2 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
User vs System Crontabs¶
User crontab (crontab -e):
- No user field (runs as the editing user)
- 5 fields + command
- Survives user profile changes
- Cannot set SHELL in the schedule line
System crontab (/etc/cron.d/):
- Has user field (6th field)
- 5 fields + user + command
- Managed by config management (Ansible, etc.)
- Can set environment variables at top
The Cron Environment Trap¶
Gotcha: Cron uses
/bin/shby default, not/bin/bash. Bashisms like[[ ]],$(( )), and process substitution<()will silently fail. Either setSHELL=/bin/bashat the top of your crontab or write POSIX-compatible scripts.
This is the number-one source of "it works when I run it manually but fails in cron":
Your shell:
PATH=/usr/local/bin:/usr/bin:/bin:/usr/local/go/bin:/home/user/.local/bin
HOME=/home/user
SHELL=/bin/bash
All your .bashrc exports are loaded
Cron's environment:
PATH=/usr/bin:/bin
HOME=/root (or the user's home)
SHELL=/bin/sh
NO .bashrc, NO .profile, NO exports
Fixes¶
# Option 1: Use absolute paths
* * * * * /usr/local/bin/python3 /opt/app/script.py
# Option 2: Set PATH in the crontab
PATH=/usr/local/bin:/usr/bin:/bin
* * * * * python3 /opt/app/script.py
# Option 3: Source your profile in the command
* * * * * . /home/user/.profile && /opt/app/script.sh
# Option 4: Use a wrapper script that sets up the environment
* * * * * /opt/app/run-in-env.sh
Systemd Timers¶
Systemd timers are the modern replacement for cron. They offer better logging, dependency management, and resource control.
Timer + Service Pair¶
Every timer needs a corresponding service unit:
# /etc/systemd/system/backup.timer
[Unit]
Description=Daily backup timer
[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
# /etc/systemd/system/backup.service
[Unit]
Description=Daily backup job
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
Group=backup
StandardOutput=journal
StandardError=journal
OnCalendar Syntax¶
OnCalendar=*-*-* 02:30:00 # daily at 2:30 AM
OnCalendar=Mon *-*-* 09:00:00 # every Monday at 9 AM
OnCalendar=*-*-01 00:00:00 # first of every month
OnCalendar=*-*-* *:00/15:00 # every 15 minutes
OnCalendar=hourly # shorthand
OnCalendar=daily # shorthand
OnCalendar=weekly # shorthand
# Validate your OnCalendar expression
systemd-analyze calendar "*-*-* 02:30:00"
# Shows next trigger times — extremely useful for debugging
# Test complex expressions
systemd-analyze calendar "Mon..Fri *-*-* 08:00:00"
Key Timer Options¶
Persistent=true # if the system was off at trigger time, run on boot
RandomizedDelaySec= # add random delay to spread load across machines
AccuracySec= # how precise the trigger needs to be (default 1min)
Managing Timers¶
# Enable and start a timer
systemctl enable --now backup.timer
# List all timers with next trigger time
systemctl list-timers --all
# Check timer status
systemctl status backup.timer
# Check the service status (last run)
systemctl status backup.service
# View logs for the service
journalctl -u backup.service --since "24 hours ago"
# Manually trigger the service (for testing)
systemctl start backup.service
Advantages Over Cron¶
Feature cron systemd timer
───────────────── ──────────────────── ──────────────────────
Logging mail or redirect journald (structured)
Dependencies none After=, Requires=
Resource limits none CPUQuota=, MemoryMax=
Missed runs lost Persistent=true
Random delay none RandomizedDelaySec=
Status cron log parsing systemctl status
Overlap prevention manual (flock) built-in (oneshot)
Calendar validation none systemd-analyze calendar
Kubernetes CronJobs¶
CronJob Spec¶
apiVersion: batch/v1
kind: CronJob
metadata:
name: database-backup
namespace: production
spec:
schedule: "30 2 * * *" # same cron syntax
timeZone: "America/New_York" # K8s 1.27+
concurrencyPolicy: Forbid # do not overlap
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
startingDeadlineSeconds: 600 # give up if 10 min late
jobTemplate:
spec:
backoffLimit: 2 # retry failed job 2 times
activeDeadlineSeconds: 3600 # kill job after 1 hour
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: backup-tool:latest
command: ["/bin/sh", "-c", "/backup.sh"]
env:
- name: DB_HOST
valueFrom:
secretKeyRef:
name: db-credentials
key: host
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "512Mi"
cpu: "500m"
Concurrency Policy¶
Allow default, multiple jobs can run simultaneously
Forbid skip new job if previous is still running
Replace kill the running job and start a new one
Default trap: The default Kubernetes CronJob
concurrencyPolicyisAllow, meaning if your job takes 10 minutes and runs every 5 minutes, you will have overlapping instances. Always explicitly setconcurrencyPolicy: Forbidfor stateful jobs like backups and database maintenance.
Forbid is almost always what you want for data-integrity-sensitive jobs. Allow is appropriate for idempotent, independent tasks.
startingDeadlineSeconds¶
If the CronJob controller misses a scheduled run (e.g., controller was down), this setting controls how late the job can start. Without it, missed runs are silently skipped.
Overlap Prevention¶
The most common scheduling disaster: a job that runs longer than its interval.
With cron: Use flock¶
# /etc/cron.d/long-job
*/5 * * * * root flock -n /var/lock/long-job.lock /usr/local/bin/long-job.sh
# flock -n: non-blocking. If lock exists, exit immediately.
# flock -w 60: wait up to 60 seconds for lock.
With systemd: Built-in¶
# oneshot services do not overlap by default
[Service]
Type=oneshot
ExecStart=/usr/local/bin/long-job.sh
If the timer fires while the previous run is still going, systemd skips the trigger.
With Kubernetes: concurrencyPolicy¶
Output and Notifications¶
Cron: Capturing Output¶
# Redirect stdout and stderr to a log file
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Send stdout to log, stderr to email (default MAILTO)
30 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log
# Set MAILTO for email notifications
MAILTO=ops@example.com
30 2 * * * /usr/local/bin/backup.sh 2>&1
# Suppress all output (use sparingly)
30 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
Systemd: journald¶
# Logs go to journal automatically
journalctl -u backup.service -n 50
# Follow logs in real-time during a run
journalctl -u backup.service -f
Quick Reference¶
Task Tool
───────────────────────────── ──────────────────────────────
Simple scheduled command cron (crontab -e)
Managed by config management /etc/cron.d/ files
Needs logging and dependencies systemd timer
Needs resource limits systemd timer
Runs in Kubernetes CronJob
Needs overlap prevention flock (cron) / Forbid (K8s)
One-time future job at
Run when system is idle batch
Fun fact: The name "cron" comes from the Greek word "chronos" (time). The cron daemon was written by Ken Thompson for Version 7 Unix in 1979. The modern Vixie Cron (written by Paul Vixie in 1987) is the version most Linux distributions use, and it introduced the
@rebootand@hourlyshorthand syntax.
Scheduled jobs are only as reliable as your monitoring of them. If nobody checks whether the backup ran, the backup did not run. Monitor your cron jobs like you monitor your services.
Wiki Navigation¶
Prerequisites¶
- Linux Ops (Topic Pack, L0)
Related Content¶
- LPIC / LFCS Exam Preparation (Topic Pack, L2) — Bash / Shell Scripting, systemd
- Linux Ops (Topic Pack, L0) — Bash / Shell Scripting, systemd
- RHCE (EX294) Exam Preparation (Topic Pack, L2) — Bash / Shell Scripting, systemd
- Advanced Bash for Ops (Topic Pack, L1) — Bash / Shell Scripting
- Bash Exercises (Quest Ladder) (CLI) (Exercise Set, L0) — Bash / Shell Scripting
- Bash Flashcards (CLI) (flashcard_deck, L1) — Bash / Shell Scripting
- Case Study: Systemd Service Flapping (Case Study, L1) — systemd
- Cron Flashcards (CLI) (flashcard_deck, L1) — Cron & Job Scheduling
- Deep Dive: Linux Boot Sequence (deep_dive, L2) — systemd
- Deep Dive: Systemd Architecture (deep_dive, L2) — systemd