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 …– sendsSIGTERMafter 1800 seconds, exit status 124 on timeout (add-k 10to follow up withSIGKILL). -
>> /var/log/myjob.log– appends timestamped start/end lines and streamsstdout/stderrof 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:
- Takes an advisory lock with
flock -n. - Optionally enforces a timeout via
timeout. - Logs start/end messages (with timestamps) and the job’s output to a file you specify.
- 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 thatcronlockdoesn’t expose (e.g., multiple locks, conditional notifications, or advanced signal handling). - Reach for
cronlockfor everyday cron jobs where you just want “one copy at a time, optional timeout, simple logging, optional webhook”.
Further reading
-
man flock– details onflock -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).