What you will learn

Guarantee a cron-scheduled script never runs two overlapping copies, even when a previous run takes longer than the schedule's interval — using flock instead of a fragile PID-file check.

Before you begin

flock ships with util-linux on virtually every Linux distribution. This is the right tool specifically for "never overlap"; for "track whether a long-lived daemon is alive," see pidfile-heartbeat-supervisor instead.

1. Re-exec the script under its own lock

#!/usr/bin/env bash
set -Eeuo pipefail

LOCK_FILE="/var/run/myapp-nightly.lock"

# Re-exec self holding an exclusive, non-blocking lock on FD 200.
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
    echo "another run is already in progress, exiting" >&2
    exit 0
fi

The lock is tied to file descriptor 200, which stays open for the life of the process — the kernel releases it automatically on exit, crash, or kill -9, so there's no stale-lock cleanup logic to get wrong (unlike a plain PID file).

2. Do the actual work inside the lock

echo "$(date -u +%FT%TZ) starting nightly job (pid $$)"

long_running_task() {
    sleep 30   # stand-in for real work
}

long_running_task

echo "$(date -u +%FT%TZ) finished"

3. Optionally wait instead of bailing, with a timeout

# Wait up to 60s for the lock instead of exiting immediately.
exec 201>"$LOCK_FILE"
if ! flock -w 60 201; then
    echo "timed out waiting for lock" >&2
    exit 1
fi

Use the non-blocking form (-n) for a job that should just skip its slot if the previous run is still going, and the timeout form (-w) when a short queue-up is acceptable but an indefinite pile-up isn't.

Example output

2026-09-19T03:00:01Z starting nightly job (pid 18422)
2026-09-19T03:00:31Z finished

second concurrent invocation:

another run is already in progress, exiting

Verify the result

Launch the script twice back to back manually (./nightly.sh & ./nightly.sh &) and confirm exactly one instance logs "starting" while the other exits immediately with the lock message.

Troubleshooting

If both instances proceed, confirm the lock file path is identical between runs (a relative path resolved from different cron working directories is a common cause) and that FD 200 isn't accidentally closed before the flock call.

Next steps

Wrap auto-log-rotation-pipeline or auto-backup-retention-vault in this pattern — anything scheduled by cron that touches shared state benefits from a concurrency guard.