Problem: overlapping and runaway cron jobs

Cron schedules tasks at fixed intervals. If a job runs longer than its interval, the next invocation starts before the previous one finishes. This can cause resource contention, corrupted data, or a job that never finishes because it is stuck.

A common solution is to use file‑based advisory locking (flock) to ensure that only one instance of a job runs at a time. In addition, you may want:

  • A hard timeout (timeout) so a hung command does not block the scheduler forever.
  • Timestamped logging to a file so you can later inspect what happened.
  • A webhook that reports failure to an external service (e.g., PagerDuty, Slack, a custom endpoint).

cronlock ties all of those primitives together in a single, tiny wrapper.

From‑scratch technique

Below is the raw pattern that cronlock implements. It helps illustrate the moving parts before we start using the wrapper.

#!/usr/bin/env bash # myjob-wrapper.sh: one run at a time, 30-min timeout, log, alert on failure LOG=/var/log/myjob.log exec 9>/tmp/myjob.lock flock -n 9 || exit 0 # another run holds the lock: skip quietly echo "[$(date '+%F %T')] myjob start" >> "$LOG" timeout 1800 /usr/local/bin/my_script.sh >> "$LOG" 2>&1 rc=$? echo "[$(date '+%F %T')] myjob end: exit $rc" >> "$LOG" if [ "$rc" -ne 0 ]; then curl -s -X POST -H 'Content-Type: application/json' \ -d "{\"job\":\"myjob\",\"exit\":$rc,\"host\":\"$(hostname)\"}" \ https://hooks.example.com/alerts fi exit "$rc" 
  • flock -n /var/lock/myjob.lock – tries to grab an exclusive lock; if the lock is already held, the command exits immediately (exit 1).
  • timeout 1800 … – sends SIGTERM after 1800 seconds, exit status 124 on timeout (add -k 10 to follow up with SIGKILL).
  • >> /var/log/myjob.log – appends timestamped start/end lines and streams stdout/stderr of the job.
  • curl … POST … – fires a small JSON payload if the job exits non‑zero.

All four concerns are now covered, but the snippet is verbose for everyday use. That’s where cronlock helps.

Enter cronlock

cronlock is a tiny Bash script that wraps the above logic. You hand it a command line, and it:

  1. Takes an advisory lock with flock -n.
  2. Optionally enforces a timeout via timeout.
  3. Logs start/end messages (with timestamps) and the job’s output to a file you specify.
  4. If the job exits non‑zero, sends a JSON POST to a webhook URL you provide.

Installation

# Copy the script somewhere on your PATH cp cronlock ~/bin/cronlock chmod +x ~/bin/cronlock 

Make sure ~/bin (or the directory you chose) is in $PATH.

Quick reference

cronlock [--name NAME] [--timeout SECONDS] [--log FILE] [--notify URL] -- COMMAND [ARGS...] 
Option Description
--name NAME Lock identifier (also used for logging). Defaults to the basename of COMMAND. Must match [A-Za-z0-9._-]+.
--timeout SECONDS Kill the wrapped command after this many whole seconds (via timeout).
--log FILE Append timestamped start/end lines and stream stdout/stderr of COMMAND to this file.
--notify URL On non‑zero exit, POST a JSON body {"job":"…","exit":…,"host":"…"} to this URL with curl.
-h, --help Show usage and exit.
-- Separator; everything after it is the COMMAND [ARGS...].

Exit codes

Code Meaning
0 The command completed successfully or a previous instance already held the lock, so this run was skipped.
2 Internal error (bad options, missing values, cannot open lock or log file).
otherwise The exit code of the wrapped COMMAND (including timeout‑generated failures).

Using cronlock – examples

All examples assume cronlock is installed as shown above.

Run a backup with a 30‑minute timeout and a dedicated log

cronlock --name backup \ --timeout 1800 \ --log ~/log/backup.log \ -- /usr/local/bin/backup.sh 

If another backup is still running when the next cron tick fires, cronlock simply exits 0 without starting a second copy.

Alert a webhook if a nightly report fails

cronlock --name nightly-report \ --notify https://hooks.example.com/cron \ -- /usr/local/bin/report.py --daily 

If report.py returns a non‑zero status, cronlock POSTs a tiny JSON payload to the supplied URL.

Keep a database sync from ever overlapping

*/5 * * * * ~/bin/cronlock --name dbsync --log ~/log/dbsync.log -- /usr/local/bin/dbsync.sh 

Only one dbsync instance will ever run; the rest are silently skipped.

Show built‑in help

cronlock --help 

When to pick raw flock vs. cronlock

  • Use raw flock (or the snippet from section 1) when you need a custom workflow that cronlock doesn’t expose (e.g., multiple locks, conditional notifications, or advanced signal handling).
  • Reach for cronlock for everyday cron jobs where you just want “one copy at a time, optional timeout, simple logging, optional webhook”.

Further reading

  • man flock – details on flock -n, lock file semantics, and advisory locking.
  • man timeout – explains how the optional grace period works.
  • curl(1) – the tool used for the optional POST notification.

cronlock is one of four MIT-licensed scripts in ‘Automation Workflow Templates v1’ at https://unixarcade.itch.io/automation-workflow-templates-v1 (MIT).