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).
Top comments (1)
Clean writeup. Two small things worth adding from production use. First, flock locks are tied to the open file descriptor and released when it closes, so make sure the descriptor holding the lock stays open for the whole command lifetime. If a subshell grabs the lock and exits early, the lock vanishes silently and a second instance can start anyway. Second, keep the lock file on a local filesystem like /var/lock (tmpfs), not NFS, where flock semantics are unreliable. And one nit: exit 0 means both 'ran fine' and 'skipped', so if you ever alert on missing successful runs, log the skip explicitly so the two are distinguishable.