Prevent Overlapping Bash Jobs with flock
When a scheduled task takes longer than expected, cron, a systemd timer, or a second manual launch can start another copy before the first one finishes. On Linux, the flock utility can hold an advisory lock for the duration of a script. This guide builds a small Bash job that exits with status 75 if another copy already holds the lock.
Check That flock Is Available
PrerequisitesCheck that the host has the expected utility and confirm the account’s home directory. This example keeps its lock under $HOME so the same account uses the same path from an interactive shell, cron, or a systemd service.
command -v flockflock --versionprintf 'HOME=%s\n' "$HOME"❯ Illustrative Output (Results Vary)
/usr/bin/flockflock from util-linux 2.xHOME=/home/samCreate a Script That Holds an Exclusive Lock
ImplementationSave this as single-instance.sh. File descriptor 9 stays open for the script, and flock tries to lock that descriptor without waiting. The ten-second sleep makes the overlap test easy; replace it with the real job after testing.
#!/usr/bin/env bashset -Eeuo pipefailumask 077
if [[ -z "${HOME:-}" ]]; then printf 'HOME is not set.\n' >&2 exit 1fi
lock_dir="$HOME/.local/run/report-job"if [[ -e "$lock_dir" || -L "$lock_dir" ]]; then [[ -d "$lock_dir" && ! -L "$lock_dir" ]] || { printf 'Lock path exists but is not a real directory; stop.\n' >&2 exit 1 }else mkdir -p -- "$lock_dir" || exit 1 chmod 700 -- "$lock_dir" || exit 1filock_owner=$(stat -c '%u' -- "$lock_dir") || exit 1lock_mode=$(stat -c '%a' -- "$lock_dir") || exit 1if [[ "$lock_owner" != "$EUID" ]] || (( (8#$lock_mode & 077) != 0 )); then printf 'Lock directory must belong to this account and be private; stop.\n' >&2 exit 1fi
lock_file="$lock_dir/report-job.lock"if [[ -L "$lock_file" ]]; then printf 'Lock file must not be a symbolic link; stop.\n' >&2 exit 1fiexec 9>>"$lock_file"
if flock --nonblock --conflict-exit-code 75 9; then :else status=$? if (( status == 75 )); then printf 'Another report job is already running.\n' >&2 else printf 'Could not acquire the lock (flock exit %s).\n' "$status" >&2 fi exit "$status"fi
printf 'Lock acquired; running the job.\n'sleep 10 # Replace with the real work after testing.printf 'Job complete.\n'❯ Illustrative Output (Results Vary)
The first process holds the lock until its file descriptor closes.Check the Script and Make It Executable
ValidationCheck the Bash syntax without running the job, then allow your account to execute the file. Run it from its containing directory for the test.
bash -n single-instance.shchmod u+x single-instance.sh❯ Illustrative Output (Results Vary)
No output and a zero exit status from bash -n mean the file passed the syntax check.Confirm a Second Run Exits Instead of Waiting
Concurrency TestOpen two terminal sessions. Start the script in the first, then start it in the second before the ten-second test sleep finishes. The first run should complete; the second should print the busy message and return status 75.
# Terminal 1./single-instance.sh
# Terminal 2, while Terminal 1 is still running./single-instance.shprintf 'exit=%s\n' "$?"❯ Illustrative Output (Results Vary)
Another report job is already running.exit=75Replace the Test Work with Your Real Job
Use in AutomationReplace the sleep command with the task, keeping it after the lock check. Use the same script and lock path for manual runs and scheduled runs. A failed nonblocking acquisition returns 75, which a scheduler can record as a skipped overlap.
# Example shape after replacing the test sleep:if ! /usr/local/bin/report-generator --output "$HOME/reports/latest.csv"; then printf 'Report generation failed.\n' >&2 exit 1fi❯ Illustrative Output (Results Vary)
The lock remains held while the foreground command runs and is released when the script exits.