Cron Job
Timeout
Scheduling Tasks
Linux Commands
Automation

Cron Job with timeout

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Cron jobs can hang indefinitely if a script stalls on network I/O, a locked resource, or a bug. The timeout command (GNU coreutils) wraps any command with a time limit, killing it if it exceeds the specified duration. Combining timeout with cron ensures that stuck jobs do not pile up, consume resources, or block the next scheduled run.

Basic Cron with Timeout

bash
1# Run a script every day at 2 AM, kill if it takes longer than 5 minutes
20 2 * * * timeout 300 /path/to/script.sh
3
4# Using human-readable durations (GNU coreutils)
50 2 * * * timeout 5m /path/to/script.sh
60 */6 * * * timeout 1h /path/to/long_running_job.sh

Duration suffixes: s (seconds, default), m (minutes), h (hours), d (days).

How timeout Works

bash
1# timeout sends SIGTERM after the duration expires
2timeout 30s ./my_script.sh
3
4# Exit codes:
5# 0   — command completed successfully within the time limit
6# 124 — command timed out (killed by timeout)
7# 137 — command was killed with SIGKILL (see --kill-after)
8
9echo $?  # Check exit code after timeout

Sending SIGKILL After SIGTERM

Some processes ignore SIGTERM. Use --kill-after to send SIGKILL as a follow-up:

bash
1# Send SIGTERM after 5 minutes, then SIGKILL 10 seconds later if still running
20 2 * * * timeout --kill-after=10s 5m /path/to/script.sh
3
4# Short form
50 2 * * * timeout -k 10s 5m /path/to/script.sh

The sequence is: wait 5 minutes, send SIGTERM, wait 10 more seconds, send SIGKILL.

Specifying the Signal

bash
1# Send SIGINT instead of SIGTERM
2timeout --signal=SIGINT 5m /path/to/script.sh
3
4# Send SIGHUP
5timeout --signal=HUP 5m /path/to/script.sh

Preventing Overlapping Cron Jobs

Use flock to ensure only one instance runs at a time:

bash
1# Run every 5 minutes, timeout at 4 minutes, no overlap
2*/5 * * * * flock -n /tmp/myjob.lock timeout 4m /path/to/script.sh
3
4# flock -n: non-blocking — if lock is held, skip this run
5# timeout 4m: kill after 4 minutes (before next cron trigger at 5 min)
bash
# With logging
*/5 * * * * flock -n /tmp/myjob.lock timeout 4m /path/to/script.sh >> /var/log/myjob.log 2>&1

Timeout in Scripts

bash
1#!/bin/bash
2# Use timeout inside a script for specific commands
3
4# Timeout a curl request
5timeout 30s curl -s https://api.example.com/data > response.json
6if [ $? -eq 124 ]; then
7    echo "API request timed out"
8    exit 1
9fi
10
11# Timeout a database query
12timeout 60s mysql -u user -p'pass' -e "SELECT * FROM large_table" > output.csv

Alternative: Using timelimit

On systems without GNU timeout:

bash
1# Install timelimit
2apt install timelimit  # Debian/Ubuntu
3
4# Usage (slightly different syntax)
5timelimit -t 300 /path/to/script.sh
6# -t: SIGTERM after N seconds
7# -T: SIGKILL after N more seconds

Cron Job with Timeout and Email Notification

bash
# Send email if the job times out
0 2 * * * timeout 5m /path/to/script.sh || echo "Job timed out or failed" | mail -s "Cron Alert" [email protected]

More robust version with a wrapper script:

bash
1#!/bin/bash
2# /path/to/run_with_timeout.sh
3
4timeout --kill-after=30s 5m /path/to/actual_script.sh
5EXIT_CODE=$?
6
7if [ $EXIT_CODE -eq 124 ]; then
8    echo "Job timed out at $(date)" >> /var/log/cron_timeouts.log
9    # Send alert
10elif [ $EXIT_CODE -ne 0 ]; then
11    echo "Job failed with exit code $EXIT_CODE at $(date)" >> /var/log/cron_failures.log
12fi
13
14exit $EXIT_CODE
bash
# Crontab entry
0 2 * * * /path/to/run_with_timeout.sh >> /var/log/myjob.log 2>&1

Systemd Timer Alternative

For more control than cron, use systemd timers with built-in timeout:

ini
1# /etc/systemd/system/myjob.service
2[Unit]
3Description=My scheduled job
4
5[Service]
6Type=oneshot
7ExecStart=/path/to/script.sh
8TimeoutStartSec=300  # Kill after 5 minutes
ini
1# /etc/systemd/system/myjob.timer
2[Unit]
3Description=Run myjob daily
4
5[Timer]
6OnCalendar=*-*-* 02:00:00
7Persistent=true
8
9[Install]
10WantedBy=timers.target
bash
sudo systemctl enable --now myjob.timer
sudo systemctl list-timers  # Verify

Common Pitfalls

  • Not accounting for cron's PATH: Cron runs with a minimal PATH. timeout may not be found if it is in /usr/local/bin. Use the full path (/usr/bin/timeout) or set PATH at the top of the crontab.
  • Timeout shorter than expected run time: If a job normally takes 4 minutes and you set a 3-minute timeout, it will be killed on every run. Set the timeout to at least 2x the normal duration to account for variability.
  • Processes ignoring SIGTERM: Some programs (Java apps, background process groups) ignore SIGTERM. Always add --kill-after=10s to send SIGKILL as a backup. Without it, the process may run indefinitely despite the timeout.
  • Not logging timeout events: Without checking $? for exit code 124, you will not know the job was killed. Always log or alert on timeouts so you can investigate the root cause.
  • Child processes surviving the timeout: timeout kills the direct child process, but grandchild processes may survive. Use timeout --foreground or setsid to ensure the entire process group is killed.

Summary

  • Use timeout 5m /path/to/script.sh in crontab to prevent hung jobs
  • Add --kill-after=10s to send SIGKILL if the process ignores SIGTERM
  • Use flock -n alongside timeout to prevent overlapping cron executions
  • Check exit code 124 to detect and log timeout events
  • Use full paths for timeout in cron (e.g., /usr/bin/timeout)
  • Consider systemd timers with TimeoutStartSec for more robust scheduling

Course illustration
Course illustration

All Rights Reserved.