Conceptual editorial illustration
AI-generated editorial illustration. The measurements and experiment results are presented separately below.

A job scheduled every five minutes can still be running when its next invocation arrives. Before adding a lock, decide what that second invocation should do: skip this opportunity, wait for the first job, or wait only briefly. Those choices change which work gets done.

A 2014 Unix & Linux question describes exactly this scheduling problem. We used it as a concrete problem statement, then checked the util-linux and GNU manuals and ran seven isolated cases. The result below is a small demonstration of command behavior on one local Linux filesystem. We did not run a cron daemon or measure scheduler reliability.

Choose what a busy lock means

The command patterns below use LOCK and WORKER as placeholders. Replace them with one stable lock-file path in a directory controlled by the job's user and an executable command. All competing invocations must use the same lock. This article uses util-linux flock and GNU timeout; commands with the same names on other systems can behave differently.

Skip if another invocation holds the lock

flock -n -E 75 LOCK WORKER

-n makes lock acquisition nonblocking. We explicitly chose exit code 75 for a busy lock using -E; the default conflict code is 1. In our busy-lock case, the second worker never started and the command returned 75.

Skipping can suit a refresh that will examine current state again later. It can lose work if each scheduled invocation represents a distinct item. A lock is not a durable queue; decide where unfinished work is recorded.

Wait for the lock, or stop waiting after a limit

flock LOCK WORKER
flock -w 0.5 -E 75 LOCK WORKER

Without -n or -w, flock waits until the lock is available. With -w 0.5, it waits up to half a second to acquire the lock. That half-second does not limit how long WORKER runs after acquisition. In our corresponding case, the worker then ran for about 0.800 seconds and completed.

Waiting avoids that immediate skip, but repeated invocations can accumulate behind slow work. Neither fairness nor a bound on the number of waiting processes was tested here. The short decimal durations in this article are experiment parameters, not recommended production limits.

Place the timeout around the phase you mean

Limit execution after acquiring the lock

flock LOCK timeout --kill-after=0.2s 0.15s WORKER

Here flock acquires the lock before starting timeout. The 0.15-second timeout covers the enclosed worker, while waiting for flock is outside that limit. GNU timeout normally sends TERM first; --kill-after=0.2s requests KILL if the command remains running another 0.2 seconds after that initial signal.

Our synthetic worker started, was interrupted before its end event, and returned through the wrapper as 124. This is the normal timeout status when --preserve-status is not used. A command requiring forced KILL can instead produce 137; this run did not exercise that branch.

Include waiting in the timeout

timeout --kill-after=0.2s 0.15s flock LOCK WORKER

Here timeout encloses flock as well. With another process holding the lock for 0.8 seconds, the contender returned 124 without starting its worker. The same exit code therefore described two different stages: interrupted execution in the previous case and expired waiting in this one.

Timeout is a signal-based limit, not a promise of exact wall-clock termination, rollback, or cleanup of arbitrary descendants. A terminated worker may already have changed data. Design its partial-write and retry behavior separately.

What the seven cases actually did

The recorded run completed on November 3, 2026 in Asia/Shanghai (2026-11-02T23:13:17Z in UTC). Environment: Linux 6.8.0-147-generic, Python 3.12.3, util-linux flock 2.39.3 and GNU coreutils timeout 9.4. Workers only wrote synthetic JSON start/end events and performed short sleeps.

CaseExitContender events
Default wait0Start after holder end; then end
Skip busy: -n -E 7575No start
Wait expires: -w 0.15 -E 7575No start
Wait limit 0.5; work delay 0.80Start, then end about 0.800s later
Timeout inside flock124Start; no end
Timeout outside flock, busy lock124No start
Worker deliberately exits 2323Start, then end

For cases with a holder, the harness waited for that holder's actual start event before launching the contender. It compared events using a monotonic clock. After each case, a fresh flock process successfully acquired the same lock again. All seven defined expectations passed; this is not seven production runs or a reliability estimate.

The holder's top-level elapsed_s includes time until the harness collects its result. Do not treat it as the holder's exact execution duration. The approximately 0.800-second worker duration comes from that worker's own start/end timestamps.

Repeat the isolated drill

  1. Use a Linux environment with Python 3.9 or later, existing util-linux flock and GNU timeout. Confirm the actual implementations with the version commands below.
  2. Download and inspect cron_overlap_demo.py. It has no option to open your existing job or database, makes no network requests and does not install a schedule.
  3. Run it with an output directory you control. It creates a fresh cron-overlap-... child directory and writes its synthetic files there.
python3 --version
flock --version
timeout --version
python3 cron_overlap_demo.py --out ./overlap-drill

Open the resulting result.json. Check each case's exit code, contender events and lock_reacquired_after_case, rather than only the overall passed flag. Compare with our recorded result. Sub-second tests can vary with system conditions; inspect a failure rather than changing the expected result to make it pass.

Keep the lock and the job's outcome separate

  • A file is not the held lock. The lock file can remain after the kernel lock is released. Do not delete it while jobs are active: a replacement file can let different processes lock different objects under the same pathname. Test acquisition rather than file existence.
  • Keep the work within the lock's lifetime. Background processes can inherit file descriptors. This drill uses foreground workers; daemonization, inherited locks and alternative close/no-fork modes require their own tests.
  • Use a local, controlled lock path. NFS and CIFS locking behavior depends on the filesystem and mount configuration. We did not test them or coordination between machines.
  • Record stages as well as codes. A real worker can itself return 75 or 124. Capture attempted start, lock outcome, worker start and completion so those numbers are not your only explanation.
  • Verify success before publishing output. Mutual exclusion does not validate a backup, make a retry idempotent or recover interrupted business work. Use explicit expectations for the result.

For the wider design decisions, use the small automation job checklist. If the job creates a database backup, also test the restored state and application reads.

Sources and scope

We checked one local Linux filesystem with synthetic foreground workers. We did not test an actual cron daemon, a distributed lease, queued delivery, real workload capacity, network filesystems, background descendants or recovery after production side effects.