A scheduled job often looks harmless until two copies run at the same time. A backup takes longer than usual, a second timer fires, and both processes start writing the same output. A maintenance script overlaps with itself and launches duplicate work. A cache refresh runs concurrently and leaves partially coordinated state behind.

The problem is not that Linux started the processes incorrectly. The problem is that the application needs a rule saying, “only one cooperating process may enter this critical section at a time.”

Linux advisory file locks provide that rule. The flock system call applies a shared or exclusive lock to an open file, and the util-linux flock command exposes the same basic idea conveniently to shell scripts and command lines.

The key word is advisory: the kernel tracks the lock, but ordinary file I/O is not automatically forbidden for a process that ignores the locking protocol. The protection works when every participant that can conflict agrees to acquire the appropriate lock first.

The useful mental model is:

The lock is a coordination token associated with an open file description. It protects whatever resource your processes agree that the lock represents; it does not have to protect the lock file’s own contents.

That distinction explains both why lock files are useful and why they are not a complete substitute for application-level consistency rules.

Start with the smallest useful exclusive lock

Suppose a report generator must never overlap with another copy of itself. A simple invocation is:

flock -x /tmp/report-generator.lock ./generate-report.sh

-x requests an exclusive lock. Exclusive is also the default mode for the flock command, but writing it explicitly makes the concurrency rule visible.

If another cooperating process already holds an incompatible lock on the same file, this command waits until the lock becomes available. Only after acquisition does flock run ./generate-report.sh.

The lock file is just a stable rendezvous point. It can be empty. Its pathname tells competing processes which kernel lock they should contend for.

This pattern is useful when:

  • overlapping executions would duplicate work or corrupt shared state;
  • every relevant entry point can use the same lock protocol; and
  • waiting for the current execution to finish is acceptable.

If those conditions do not hold, a different coordination mechanism may be required.

Understand what the lock actually guarantees

An exclusive flock lock prevents another cooperating process from successfully acquiring an incompatible flock lock on the same file at the same time.

It does not mean that other processes cannot open, read, write, rename, or delete that file merely because the lock exists. On a normal local Linux filesystem, flock locks are advisory. A process that never calls flock can usually ignore them.

That means this is unsafe as a complete protocol:

# Process A uses a lock.
flock -x /tmp/report.lock ./generate-report.sh

# Process B writes the shared output without taking the lock.
./rewrite-report-directly.sh

Process A and Process B are not coordinated. The first command’s lock cannot protect against code that does not participate.

The preferred design is to define the lock as part of the resource’s contract:

Before reading or changing the shared report state:
1. open the agreed lock file;
2. acquire the required lock mode;
3. access the shared state;
4. release the lock by unlocking or closing the descriptor.

The protected resource might be a file, a directory tree, a local cache, a device operation, or a job that should have only one active instance. The lock pathname is simply the name all participants use to coordinate.

Choose between waiting and failing fast

Blocking until a lock becomes available is often correct for short operations. It can be a bad operational choice for scheduled jobs, however, because waiting processes may accumulate when one execution gets stuck.

Use -n to make acquisition nonblocking:

if ! flock -n -x /tmp/report-generator.lock ./generate-report.sh; then
    echo 'report generation is already running' >&2
    exit 1
fi

With -n, flock fails instead of waiting when an incompatible lock is already held. The command is not started in that case.

This is usually a better fit for periodic work where one skipped run is preferable to a queue of delayed runs. For example, if a cache refresh occurs every minute but normally finishes in ten seconds, a second invocation may simply skip when the previous refresh is unexpectedly still active.

A third option is a bounded wait:

flock -w 15 -x /tmp/report-generator.lock ./generate-report.sh

-w 15 waits for at most 15 seconds. If the lock still cannot be acquired, flock exits without running the command.

Choose the behavior from the application’s semantics, not from convenience:

  • wait indefinitely when every execution must eventually run and blocking cannot create an operational pile-up;
  • use a timeout when short contention is acceptable but unbounded waiting is not;
  • fail immediately when a concurrent execution makes the new run redundant.

Use a dedicated descriptor inside a shell script

Locking an entire external command is simple, but scripts sometimes need the lock to cover only part of their work. In Bash, you can open a lock file on a dedicated file descriptor and then lock that descriptor:

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

exec 9>/tmp/report-generator.lock

if ! flock -n -x 9; then
    echo 'another report generator is active' >&2
    exit 1
fi

# Only this critical section requires exclusivity.
read_source_data
write_report

exec 9>/tmp/report-generator.lock opens the file and keeps file descriptor 9 in the shell. flock -x 9 applies the lock to that already-open file.

The shell holds the descriptor for the rest of the process unless it explicitly closes it. When the process exits and the final descriptor referring to that open file description is closed, the lock is released automatically.

You can shorten the lock lifetime by closing the descriptor after the critical section:

flock -x 9
update_shared_state
flock -u 9
exec 9>&-

Explicit flock -u is usually unnecessary when the descriptor is about to be closed, but it can make an intentional early release clear. What matters is that code after the release must no longer depend on exclusivity.

Lock lifetime follows open-file-description lifetime

A common mistake is to think the lock belongs only to the integer descriptor number used to acquire it. On Linux, flock() locks are associated with an open file description: the kernel object created by an open()-style operation.

Multiple file descriptors can refer to the same open file description, for example after dup() or when a descriptor is inherited across fork(). Those descriptors refer to the same flock lock. Closing just one duplicate does not necessarily release it; the lock is released when the final descriptor referring to that open file description is closed, or when the lock is explicitly removed.

This matters when scripts or programs launch child processes. A child can inherit an open descriptor and therefore keep the lock alive longer than the parent expects.

For the command-line flock utility, --close can be useful when the child command should not inherit the locking descriptor:

flock --close -x /tmp/report-generator.lock ./generate-report.sh

The utility retains responsibility for the lock while the command runs, but closes the lock descriptor in the executed command. This is especially relevant if the command starts long-lived background children that should not accidentally extend the lock’s lifetime.

Do not add --close automatically. If the executed program itself needs access to the locking descriptor, closing it would defeat that design.

Shared locks allow concurrent readers

Not every critical section needs exclusivity. flock also supports shared locks with -s.

A shared lock is compatible with other shared locks, but incompatible with an exclusive lock. That gives you a readers-writer pattern:

# Reader
flock -s /tmp/catalog.lock ./read-catalog.sh

# Writer
flock -x /tmp/catalog.lock ./update-catalog.sh

Multiple readers may hold the shared lock at once. A writer requesting the exclusive lock must wait until conflicting locks are gone.

This pattern only helps when readers and writers use the same protocol. A reader that skips the shared lock can race with an updating writer.

It is also important not to assume a particular fairness policy. Your correctness should not depend on readers or writers being granted the lock in a specific scheduling order.

Avoid lock upgrades as a correctness primitive

It can be tempting to acquire a shared lock, inspect state, and then convert it to an exclusive lock only if a write is needed:

shared lock -> inspect -> exclusive lock -> modify

On Linux flock(), converting between shared and exclusive modes is not guaranteed to be atomic. The existing lock can be removed before the new mode is established, allowing another waiter to acquire a lock in between.

If correctness requires that nothing relevant change between inspection and modification, acquire the exclusive lock before the decision:

flock -x /tmp/catalog.lock ./inspect-and-maybe-update.sh

This may reduce concurrency because readers that would otherwise coexist now serialize behind the exclusive lock. That is a real trade-off, but it is usually preferable to building correctness on an upgrade guarantee that does not exist.

For workloads where read-mostly concurrency is essential and upgrades are frequent, consider a coordination design built specifically for that access pattern rather than relying on lock conversion.

Keep the protected operation and the lock name stable

A lock protocol fails when different code paths choose different lock files for the same resource.

For example, these two commands do not coordinate:

flock -x /tmp/report.lock ./job-a.sh
flock -x /tmp/report-generator.lock ./job-b.sh

Even if both scripts modify the same report, the kernel sees locks on different files.

Choose one stable lock pathname and treat it as part of the operational interface. System-wide services often use a runtime directory such as /run when permissions and lifecycle are configured appropriately. Per-user tools can use a user-owned runtime or state directory.

Avoid placing a security-sensitive lock in a directory where untrusted users can freely replace pathname components. File ownership, directory permissions, symlink handling, and startup cleanup are separate concerns from advisory locking.

The lock also should cover exactly the operation that requires coordination. Holding it around unrelated network requests or expensive computation increases contention. Releasing it too early exposes the shared state before the operation is actually complete.

A useful question is: what invariant must be true while this lock is held? Keep the critical section aligned with that invariant.

Do not confuse mutual exclusion with durable file updates

An exclusive lock can ensure that cooperating writers run one at a time. It does not automatically make each writer’s filesystem update crash-safe.

Suppose a process holds a lock and overwrites a configuration file in place. If the machine loses power halfway through the write, no competing writer caused the damage; the update itself was not durable or atomic enough for the application’s requirements.

For important file replacement, you may need a separate write protocol such as writing a temporary file, synchronizing it as required, and atomically replacing the destination with rename(). The lock can coordinate multiple writers, while the replacement protocol handles crash consistency. They solve different failure modes.

Likewise, a lock does not create a transaction across several unrelated resources. If an operation updates a file and a database, releasing the file lock does not make those two updates atomic as a unit.

Treat network filesystems as a separate deployment case

Local Linux behavior is not the whole story. flock semantics can differ when the file lives on NFS or SMB/CIFS because the kernel may emulate the lock using other network-locking mechanisms.

On modern Linux, NFS and SMB support can cause flock to interact with fcntl-style byte-range locking, and SMB can produce behavior that is effectively mandatory for conflicting I/O from another descriptor. Mount options and server behavior can also matter.

Therefore, do not validate a lock protocol only on a local ext4 or XFS development machine and assume identical behavior on every remote filesystem. If coordination must work across hosts, test the actual filesystem, mount configuration, server, and failure modes used in production.

For distributed coordination where correctness must survive host failure, network partitioning, leases, or leader changes, a local file lock may be the wrong primitive entirely. Use a coordination mechanism whose failure model matches the distributed system.

Inspect active locks when debugging

When a job appears stuck waiting for a lock, lslocks can help inspect locks known to the kernel:

lslocks

Its default output is useful interactively, but scripts should not depend on the default column layout because that output may change. Select explicit fields or another stable interface when machine parsing matters.

Also remember that a lock’s owner may not map cleanly to exactly one process. flock locks are attached to open file descriptions, which can be shared by descriptors inherited across process boundaries.

Operational debugging therefore needs two questions:

  1. which lock is blocking acquisition?
  2. which processes still hold descriptors referring to the open file description that owns it?

The second question explains cases where the process that originally acquired a lock has exited but a descendant still keeps the relevant descriptor open.

Common mistakes to avoid

The most important failures are protocol failures rather than syntax errors.

First, do not assume the presence of a lock file means the resource is locked. The file may remain on disk after every process has released its kernel lock. Check lock acquisition, not pathname existence.

Second, do not delete a lock file as a way to break a live lock. A process can continue holding a lock on the already-open file object while another process creates a new file at the same pathname and locks that different object. You can then accidentally have two groups that believe they hold “the same” lock.

Third, do not hold a lock across more work than necessary. Long critical sections turn occasional contention into routine serialization and increase the consequences of hung processes.

Fourth, do not rely on advisory locking when participants are untrusted or cannot be changed to cooperate. Advisory locks coordinate willing participants; they are not an access-control boundary.

Finally, do not treat flock, traditional POSIX fcntl record locks, and Linux open-file-description locks as interchangeable APIs. Their ownership and lifetime rules differ, and their interaction can depend on filesystem and platform behavior.

When flock is a good fit

flock works well for local coordination problems where all participants can follow one simple locking convention. Typical examples include preventing overlapping cron or timer jobs, serializing maintenance scripts, coordinating access to a local cache, and protecting a short filesystem critical section.

A simpler design is better when no concurrency exists or the operation is naturally idempotent and harmless to repeat. A database transaction is often better when the state already lives in a database and the database can enforce the invariant directly. A process-level mutex is better when all competing work occurs inside one process. A distributed lease or consensus-backed coordinator is more appropriate when several machines must agree under network and host failures.

The decision is not “locks are good” or “locks are bad.” The useful question is whether the lock’s scope, lifetime, enforcement model, and failure behavior match the resource you are protecting.

Conclusion

Linux flock is a small primitive with a precise job: coordinate cooperating processes through shared or exclusive advisory locks.

Use a stable lock file, make every conflicting participant follow the protocol, choose deliberately between waiting and failing, and keep the critical section aligned with the invariant you need to protect. Remember that the lock follows open-file-description lifetime, that lock conversion is not an atomic upgrade guarantee, and that remote filesystems can change important semantics.

With those boundaries understood, flock is an effective way to prevent overlapping local jobs without introducing a heavier coordination system.