At 10:00, a report job starts rebuilding a shared working directory. At 10:05, its next scheduled run begins. The first process is still uploading. Now two processes can delete, replace or publish each other’s files.

Moving the schedule to every 10 minutes buys time. It does not establish who may enter that directory. For a job running on one Unix host, an advisory file lock can make that decision before either process touches the shared work.

Acquire Before Doing Any Work

An advisory lock is an agreement between cooperating processes. Every entry point must acquire the same lock. A manual invocation that bypasses it can still interfere.

Python exposes the operating system’s locking operation through fcntl.flock. LOCK_EX requests exclusive access. Adding LOCK_NB makes the attempt return immediately if another process holds an incompatible lock.

Save this complete example as overlap_demo.py. It uses the Python standard library on Unix:

import argparse
import errno
import fcntl
import sys
from contextlib import contextmanager
class AlreadyRunning(Exception):
    pass
@contextmanager
def single_run(path):
    with open(path, "a") as handle:
        try:
            fcntl.flock(handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
        except OSError as exc:
            if exc.errno in (errno.EACCES, errno.EAGAIN):
                raise AlreadyRunning(path) from exc
            raise
        yield
def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("lock_path")
    parser.add_argument("--hold", action="store_true")
    args = parser.parse_args()
    try:
        with single_run(args.lock_path):
            print("RUNNING", flush=True)
            if args.hold:
                input("Press Enter to finish.\n")
            print("FINISHED")
    except AlreadyRunning:
        print("SKIPPED_BUSY", file=sys.stderr)
        return 75
    return 0
if __name__ == "__main__":
    raise SystemExit(main())

Opening in append mode creates the lock file if needed without truncating it. The file stays open throughout the with single_run(...) block. Leaving that block closes the handle, including when the protected work raises an exception.

Only the expected contention errors become AlreadyRunning. A missing directory or another unexpected failure remains an error, rather than being reported as an ordinary skipped run.

Make the Second Process Lose

Create a private directory for the demonstration:

mkdir -m 700 "$HOME/cron-lock-demo"
python3 overlap_demo.py "$HOME/cron-lock-demo/report.lock" --hold

The first terminal prints RUNNING and waits for Enter. Leave it waiting. In a second terminal, from the same code directory, run:

python3 overlap_demo.py "$HOME/cron-lock-demo/report.lock"
echo $?

The second process prints SKIPPED_BUSY, then the shell prints 75. It never reaches the work block. Return to the first terminal and press Enter. Run the second command again: It now prints RUNNING, FINISHED and an exit status of 0.

For the real job, replace the demonstration’s print/input work with the report operation. Keep the entire operation, including any child work that must finish, inside the protected block. Create the lock directory under the service account’s control and configure the same absolute lock path for scheduled and manual runs.

Here, status 75 identifies a busy attempt. Decide how the scheduler and monitoring should handle it. Skipping may be right when the running job produces the latest report. It is wrong when every invocation represents a distinct obligation that must eventually execute. That workload needs a queue or another explicit retry policy.

Leave the Lock File Alone

The file’s existence does not mean the job is running. The operating system holds the lock against an open file, not against a permanent claim written into its name.

Deleting an apparently stale lock file can break the protection. Process A may still hold the original file open. Process B then creates a new file at the same path and locks that different file. Both processes can enter their work blocks.

Keep the lock file in place between runs. Exclude it from cleanup and deployment routines that replace files. In the example, ending the sole holder process releases the lock without deleting the file. On Linux, the lock lasts until all corresponding open file descriptors close, so forked children retaining a descriptor also matter.

Check the Deployment Boundary

This example coordinates cooperating processes using the same local file. Two containers with separate filesystems can each acquire their own lock. Network filesystems also have different locking behavior. A local demonstration does not establish coordination across hosts.

Before scheduling the repaired job, repeat the two-terminal exercise using its actual service account, entry point and lock path. Hold the first run long enough to overlap the second. Confirm both the skipped attempt and a successful run after release. That is the behavior the schedule needs to preserve.

Frequently Asked Questions

1. How can you prevent two cron jobs from running simultaneously?
Use an advisory file lock with Python's fcntl.flock to ensure only one process enters the protected work block at a time. Competing runs can exit immediately.
2. What happens if a Python process crashes while holding a file lock?
The operating system releases the lock when all corresponding open file descriptors are closed. The lock file can safely remain on disk.
3. Can Python file locks prevent overlapping jobs across multiple servers?
Not reliably with a simple local-file approach. Distributed workloads require shared coordination with suitable locking guarantees, such as a distributed lock service or centralized job scheduler.