Vucense

Cron Jobs and systemd Timers on Ubuntu 24.04: Complete Guide 2026

🟢Beginner

Schedule tasks with cron and systemd timers on Ubuntu 24.04 LTS. Covers crontab syntax, user and system cron, systemd .timer units, anacron, logging, and migrating from cron to systemd timers.

Noah Choi

Author

Noah Choi

Linux & Cloud Native Infrastructure Engineer

Published

Duration

Reading

17 min

Build

20 min

Cron Jobs and systemd Timers on Ubuntu 24.04: Complete Guide 2026
Article Roadmap

Key Takeaways

  • Five-field syntax: MIN HOUR DOM MON DOW command — minute, hour, day-of-month, month, day-of-week. Use * for “every”, */5 for “every 5”, 1,15 for “1st and 15th”, 1-5 for “1 through 5”.
  • Always full paths in cron: Cron’s PATH is /usr/bin:/bin. Commands like docker, python3, node need full paths (/usr/bin/docker) or an explicit PATH= line at the top of the crontab.
  • Redirect output: A cron job with no output redirection sends email to the local root mailbox. Add >> /var/log/myjob.log 2>&1 or > /dev/null 2>&1 to control where output goes.
  • Prefer systemd timers for new system services: Better logging (journald), catch-up on missed runs, dependency management, and systemctl integration make timers the right choice for services that run as part of the OS.

Introduction

Direct Answer: How do I schedule a cron job on Ubuntu 24.04 in 2026?

Run crontab -e to open your user’s crontab in the default editor. Add a line with the format MIN HOUR DOM MON DOW /full/path/to/command. For example, 0 3 * * * /usr/bin/python3 /home/user/backup.py >> /var/log/backup.log 2>&1 runs a Python backup script every day at 3:00 AM. The five time fields are: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–7, where 0 and 7 both mean Sunday). Use * for “every unit”, */5 for “every 5 units”, and 1,15 for “on the 1st and 15th”. Verify with crontab -l to list current jobs. Check cron daemon logs with grep CRON /var/log/syslog | tail -20. For system-wide jobs (not tied to a user), create a file in /etc/cron.d/ with an additional username field: 0 3 * * * root /usr/bin/python3 /opt/backup.py.


Part 1: Crontab Syntax

┌────────────── minute       (0–59)
│  ┌─────────── hour         (0–23)
│  │  ┌──────── day of month (1–31)
│  │  │  ┌───── month        (1–12 or JAN–DEC)
│  │  │  │  ┌── day of week  (0–7, 0 and 7 = Sunday, or SUN–SAT)
│  │  │  │  │
*  *  *  *  *   /path/to/command arg1 arg2

Common patterns:

# Every minute
* * * * * /usr/bin/command

# Every 5 minutes
*/5 * * * * /usr/bin/command

# Every day at 3:30 AM
30 3 * * * /usr/bin/command

# Every weekday (Mon–Fri) at 9:00 AM
0 9 * * 1-5 /usr/bin/command

# Every Monday at midnight
0 0 * * 1 /usr/bin/command

# 1st and 15th of every month at 6:00 AM
0 6 1,15 * * /usr/bin/command

# Every hour during business hours (9am–6pm) on weekdays
0 9-18 * * 1-5 /usr/bin/command

# Every 30 minutes between midnight and 6am
*/30 0-6 * * * /usr/bin/command

# Shorthand aliases (supported by vixie-cron)
@reboot   /usr/bin/command    # Run once at startup
@daily    /usr/bin/command    # Same as: 0 0 * * *
@weekly   /usr/bin/command    # Same as: 0 0 * * 0
@monthly  /usr/bin/command    # Same as: 0 0 1 * *
@hourly   /usr/bin/command    # Same as: 0 * * * *

Validate with crontab.guru:

# Test your expression locally (shows next 5 execution times)
python3 -c "
from datetime import datetime
import re
# Basic visualization — use crontab.guru for full validation
exprs = [
    ('*/5 * * * *',   'Every 5 minutes'),
    ('0 3 * * *',     'Daily at 3am'),
    ('0 9 * * 1-5',   'Weekdays 9am'),
    ('0 0 1,15 * *',  '1st and 15th'),
]
for expr, desc in exprs:
    print(f'  {expr:20s}  → {desc}')
"

Expected output:

  */5 * * * *           → Every 5 minutes
  0 3 * * *             → Daily at 3am
  0 9 * * 1-5           → Weekdays 9am
  0 0 1,15 * *          → 1st and 15th

Part 2: User Crontab — crontab -e

# Edit your user's crontab
crontab -e   # Opens in $EDITOR (nano by default)

# List current jobs
crontab -l

# Remove all cron jobs for current user (careful!)
# crontab -r

# View another user's crontab (requires root)
sudo crontab -l -u www-data

Adding your first cron job:

# Create a simple script to schedule
cat > ~/scripts/daily-cleanup.sh << 'EOF'
#!/bin/bash
set -euo pipefail
LOG="/var/log/daily-cleanup.log"
echo "[$(date)] Starting cleanup" >> "$LOG"

# Delete files older than 7 days in /tmp
find /tmp -type f -mtime +7 -delete 2>/dev/null || true

# Rotate logs older than 30 days
find /var/log -name "*.log.*" -mtime +30 -delete 2>/dev/null || true

echo "[$(date)] Cleanup done" >> "$LOG"
EOF

chmod +x ~/scripts/daily-cleanup.sh

# Add to crontab — run at 2:00 AM daily
(crontab -l 2>/dev/null; echo "0 2 * * * /home/$USER/scripts/daily-cleanup.sh") | crontab -
crontab -l

Expected output:

0 2 * * * /home/ubuntu/scripts/daily-cleanup.sh

Critical: Always set PATH in cron scripts:

# Cron's default PATH is minimal — add this to the TOP of your crontab
(crontab -l 2>/dev/null | head -0; \
 echo 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'; \
 echo 'SHELL=/bin/bash'; \
 crontab -l 2>/dev/null) | crontab -

crontab -l

Expected output:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SHELL=/bin/bash
0 2 * * * /home/ubuntu/scripts/daily-cleanup.sh

Part 3: System Cron — /etc/cron.d/

For system-level jobs not tied to a specific user, use /etc/cron.d/ — these run as any specified user and persist across user account changes.

# Create a system cron job (requires root)
sudo tee /etc/cron.d/db-backup << 'EOF'
# Database backup — runs at 3:00 AM as the postgres user
# Format: MIN HOUR DOM MON DOW USER COMMAND
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# PostgreSQL backup — daily at 3am
0 3 * * * postgres /usr/bin/pg_dumpall --clean | gzip > /var/backups/postgres-$(date +\%Y\%m\%d).sql.gz

# Cleanup old backups — weekly on Sunday at 4am
0 4 * * 0 root find /var/backups -name "postgres-*.sql.gz" -mtime +30 -delete
EOF

# Verify syntax (cron doesn't validate until runtime, so check manually)
sudo cat /etc/cron.d/db-backup

Expected output:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * postgres /usr/bin/pg_dumpall --clean | gzip > /var/backups/postgres-$(date +%Y%m%d).sql.gz
0 4 * * 0 root find /var/backups -name "postgres-*.sql.gz" -mtime +30 -delete

Pre-existing cron directories:

ls -la /etc/cron.{hourly,daily,weekly,monthly}/

Expected output:

/etc/cron.daily:
logrotate
man-db
apt-compat
dpkg

/etc/cron.weekly:
man-db

/etc/cron.monthly:
(empty)

Drop executable scripts directly into these directories — they run at the appropriate interval without needing a crontab entry.

# Example: create a daily cleanup script
sudo tee /etc/cron.daily/app-cleanup << 'EOF'
#!/bin/bash
# Runs daily via /etc/cron.daily
set -euo pipefail
find /opt/app/tmp -type f -mtime +1 -delete
find /var/log/app -name "*.log" -mtime +90 -delete
EOF
sudo chmod +x /etc/cron.daily/app-cleanup

Part 4: Monitoring Cron Jobs

# View cron execution logs
grep CRON /var/log/syslog | tail -20

Expected output:

Apr 22 02:00:01 hetzner CRON[12345]: (ubuntu) CMD (/home/ubuntu/scripts/daily-cleanup.sh)
Apr 22 02:00:01 hetzner CRON[12345]: (CRON) info (No MTA installed, discarding output)
# View cron journal (systemd-based Ubuntu 24.04)
journalctl -u cron --since today | tail -20

# List all user crontabs on the system (root only)
for user in $(cut -f1 -d: /etc/passwd); do
  crontab -l -u "$user" 2>/dev/null | grep -v "^#" | grep -v "^$" | \
    while read -r line; do echo "  [$user] $line"; done
done

# Check if cron is running
systemctl is-active cron && echo "Cron is running"

Expected output:

Cron is running

Part 5: systemd Timers

systemd timers are the modern alternative to cron. They log to journald, support catch-up execution, and integrate with systemctl.

How systemd timers work: A timer unit (.timer) triggers a service unit (.service). The service contains the command to run; the timer contains the schedule.

# Create a service unit (the command to run)
sudo tee /etc/systemd/system/db-backup.service << 'EOF'
[Unit]
Description=PostgreSQL Database Backup
After=postgresql.service
Requires=postgresql.service

[Service]
Type=oneshot
User=postgres
ExecStart=/usr/bin/pg_dumpall --clean
StandardOutput=append:/var/log/db-backup.log
StandardError=append:/var/log/db-backup.log
EOF

# Create the timer unit (the schedule)
sudo tee /etc/systemd/system/db-backup.timer << 'EOF'
[Unit]
Description=Run PostgreSQL backup daily at 3am
Requires=db-backup.service

[Timer]
# Run at 3:00 AM every day
OnCalendar=*-*-* 03:00:00

# If the timer was missed (e.g., server was off), run immediately on next boot
Persistent=true

# Randomise the start time by up to 5 minutes (avoids thundering herd)
RandomizedDelaySec=300

[Install]
WantedBy=timers.target
EOF

# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer

# Verify it's active and shows next trigger time
sudo systemctl status db-backup.timer --no-pager

Expected output:

● db-backup.timer - Run PostgreSQL backup daily at 3am
     Loaded: loaded (/etc/systemd/system/db-backup.timer; enabled)
     Active: active (waiting) since Wed 2026-04-22 09:00:00 UTC
    Trigger: Thu 2026-04-23 03:00:00 UTC; 17h left
   Triggers: ● db-backup.service
# List all active timers
systemctl list-timers

Expected output:

NEXT                        LEFT       LAST                        PASSED  UNIT
Thu 2026-04-23 03:00:00 UTC 17h left   Wed 2026-04-22 03:00:00 UTC 6h ago  db-backup.timer
Thu 2026-04-23 00:00:00 UTC 14h left   Wed 2026-04-22 00:00:00 UTC 9h ago  logrotate.timer

Part 6: OnCalendar Syntax Reference

OnCalendar=daily                    # Every day at midnight (0:00:00)
OnCalendar=weekly                   # Every Monday at midnight
OnCalendar=monthly                  # 1st of each month at midnight
OnCalendar=hourly                   # Every hour at :00

OnCalendar=*-*-* 03:00:00          # Every day at 3am
OnCalendar=*-*-* 03:30:00          # Every day at 3:30am
OnCalendar=Mon *-*-* 04:00:00      # Every Monday at 4am
OnCalendar=Mon..Fri 09:00:00       # Weekdays at 9am
OnCalendar=Sat,Sun 08:00:00        # Weekends at 8am
OnCalendar=*-*-1 00:00:00          # 1st of each month at midnight
OnCalendar=*-*-1,15 00:00:00       # 1st and 15th at midnight
OnCalendar=*:0/5                    # Every 5 minutes
OnCalendar=*:0/15                   # Every 15 minutes
OnCalendar=*-01-01 00:00:00        # Every year on Jan 1st

Verify an expression:

# Test OnCalendar syntax before deploying
systemd-analyze calendar "*-*-* 03:00:00"

Expected output:

  Original form: *-*-* 03:00:00
Normalized form: *-*-* 03:00:00
    Next elapse: Thu 2026-04-23 03:00:00 UTC
       (in UTC): Thu 2026-04-23 03:00:00 UTC
       From now: 17h 0min left

Part 7: Real-World Timer — Docker Image Cleanup

# Remove unused Docker images weekly to reclaim disk space
sudo tee /etc/systemd/system/docker-cleanup.service << 'EOF'
[Unit]
Description=Remove unused Docker images and volumes
After=docker.service
Requires=docker.service

[Service]
Type=oneshot
ExecStart=/usr/bin/docker system prune --all --volumes --force
StandardOutput=journal
StandardError=journal
EOF

sudo tee /etc/systemd/system/docker-cleanup.timer << 'EOF'
[Unit]
Description=Weekly Docker cleanup
Requires=docker-cleanup.service

[Timer]
OnCalendar=Sun 03:00:00
Persistent=true
RandomizedDelaySec=600

[Install]
WantedBy=timers.target
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now docker-cleanup.timer

# Manually trigger (test without waiting for schedule)
sudo systemctl start docker-cleanup.service

# View output
journalctl -u docker-cleanup.service --since today

Expected output (after manual trigger):

Apr 22 10:00:01 server docker-cleanup[12345]: Deleted Containers:
Apr 22 10:00:01 server docker-cleanup[12345]: Deleted Images:
Apr 22 10:00:01 server docker-cleanup[12345]: Total reclaimed space: 2.34GB

Cron vs systemd Timers

FeatureCronsystemd Timer
LoggingSyslog onlyjournald (queryable)
Missed run recoveryNoYes (Persistent=true)
DependenciesNoYes (After=, Requires=)
Enable/disableManual editsystemctl enable/disable
Run statusgrep CRON /var/log/syslogsystemctl status name.timer
Per-user schedulingYes (crontab)Limited
Catch-up on bootOnly with anacronBuilt-in
Minimum interval1 minute1 second

Decision guide:

  • Use cron for: simple user-level tasks, quick one-liners, scripts that don’t need dependency management
  • Use systemd timers for: system services, tasks with dependencies (e.g., PostgreSQL must be up), tasks where you need logging and catch-up

Troubleshooting

Cron job doesn’t run

Check 1: systemctl is-active cron — cron daemon must be running. Check 2: grep CRON /var/log/syslog — look for CMD entries or errors. Check 3: Test the command manually as the cron user: sudo -u www-data /full/path/to/command. Check 4: PATH issue — add PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin at the top of the crontab.

systemd timer not firing

sudo systemctl status myapp.timer       # Check active and next trigger
systemctl list-timers | grep myapp      # Confirm it's listed
journalctl -u myapp.service -n 50       # Check last run output
sudo systemctl daemon-reload            # Required after editing unit files
sudo systemctl restart myapp.timer      # Force restart

Cron output missing

Cause: If the system has no MTA (mail transfer agent), cron output is discarded with No MTA installed, discarding output. Fix: Redirect output in the crontab: 0 3 * * * /path/cmd >> /var/log/mycron.log 2>&1


Conclusion

You now have two reliable scheduling tools: cron for quick user-level tasks (edit with crontab -e, validate at crontab.guru, add PATH at the top), and systemd timers for production services (.service + .timer pair, managed with systemctl, logged to journald). Use systemd timers for all new system-level scheduled work in 2026.

Schedule the scripts you built in Bash Scripting Guide 2026 and the Python automation scripts from Python for DevOps Automation.


People Also Ask

How do I check if a cron job ran successfully?

Check the syslog: grep CRON /var/log/syslog | grep "CMD.*your-script". This shows the time cron attempted to run the job. For output, redirect the script’s stdout/stderr to a log file: 0 3 * * * /path/script.sh >> /var/log/script.log 2>&1, then check that log after the scheduled time. With systemd timers, use journalctl -u servicename.service --since "24h ago" for full output.

What is the difference between /etc/crontab and crontab -e?

crontab -e edits the current user’s personal crontab, stored in /var/spool/cron/crontabs/USERNAME. /etc/crontab is the system-wide crontab with an extra USER field: MIN HOUR DOM MON DOW USER COMMAND. Don’t edit /etc/crontab directly for application jobs — use /etc/cron.d/ files (same format as /etc/crontab) for system jobs, keeping each application’s schedule in its own file.

How do I run a cron job every 10 seconds?

Cron’s minimum resolution is 1 minute. To run every 10 seconds, add three entries offset by sleep: * * * * * /path/cmd; sleep 10; /path/cmd; sleep 10; /path/cmd. This is a workaround — for sub-minute scheduling, use a systemd timer with OnBootSec=10s and OnUnitActiveSec=10s, or a dedicated process manager like supervisord.


Further Reading


Tested on: Ubuntu 24.04 LTS. Cron (cronie) 1.5.7, systemd 255. Last verified: April 22, 2026.

Further Reading

All Dev Corner

Comments