A crontab line that never overlaps
A run that lands on a still-running previous one is quietly skipped.
flock holds a file lock while a command runs, so a second copy of the script waits for it or exits at once.
flock -n /run/lock/clean.lock clean.sh
busy — exit with code 1
flock -w 30 /run/lock/clean.lock clean.sh
wait up to 30 seconds
flock /run/lock/clean.lock clean.sh
wait as long as it takes
flock -n -E 75 /run/lock/clean.lock cmd
busy — code 75
flock -n -o /run/lock/app.lock cmd
children get no lock
lslocks -o COMMAND,PID,TYPE,PATH
who holds which locks
A scheduled script can start while the previous run is still going: the cleanup took longer than usual, someone ran it by hand, or the job sits in both cron and a timer. Two copies delete the same files and interleave their lines in one log.
flock opens a lock file, takes a lock on it from the
kernel, and only then runs the command. While the command runs, the lock is
held. A second flock on the same file waits or, with -n, exits at once with
code 1. It prints nothing when it does:
Once it holds the lock, flock returns the command's own
exit code.
| Called as | Lock is held | Code if it gives up |
|---|---|---|
flock FILE cmd |
waits for release | — |
flock -n FILE cmd |
exits at once | 1 or the -E value |
flock -w 30 FILE cmd |
waits up to 30 seconds | 1 or the -E value |
The lock lives in an open file descriptor. When the
process ends in any way, even by kill -9, the kernel closes the descriptor and
releases the lock. So a crash never leaves a stale lock behind. The file itself
stays on disk; there is no need to delete it.
The lock is advisory: the kernel does not stop anyone from writing to the file
and ignores programs that never call flock. The protection works when every
run takes the lock on the very same file.
/run/lock (also reachable as /var/lock) is a good home for lock files: it
lives in memory, any user can write there, and it is empty after a reboot.
The main options are in the table below. The full list is in man flock.
| Option | What it does | Example |
|---|---|---|
-n --nonblock |
Do not wait; if the lock is busy, exit with code 1 silently. | flock -n /run/lock/clean.lock clean.sh |
-w --timeout SECONDS |
Wait for the lock at most this many seconds, then exit with 1. | flock -w 60 /run/lock/backup.lock backup.sh |
-E --conflict-exit-code N |
Exit code when the lock was not acquired (instead of 1). | flock -n -E 75 /run/lock/x.lock cmd |
-o --close |
Close the descriptor before running the command, so children do not hold the lock. | flock -n -o /run/lock/app.lock app-start.sh |
-c --command STRING |
Run a string through the shell — several commands under one lock. | flock -n /run/lock/x.lock -c 'step1 && step2' |
FD |
Lock a descriptor already open in the script instead of a file and command. | exec 9>/run/lock/x.lock; flock -n 9 |
-o --output LIST |
Which lslocks columns to show; PATH gives the full lock file path. | lslocks -o COMMAND,PID,TYPE,PATH |
-s --shared |
Shared lock — several readers may hold it at once. | flock -s /run/lock/data.lock cat data.csv |
-x --exclusive |
Exclusive lock; this is the default. | flock -x /run/lock/data.lock update.sh |
-u --unlock FD |
Release the lock on a descriptor before the script exits. | flock -u 9 |
Nothing found. Try a shorter query.
A run that lands on a still-running previous one is quietly skipped.
exec 9>file opens descriptor 9 for the whole script, and flock -n 9 locks
it. The script is protected however it is started: by hand, from cron or by a
timer. Pick the code for a busy lock on purpose: 1 makes the attempt show up as
a failure in systemctl status and monitoring, 0 treats the skip as normal.
systemd never starts a second copy of the same unit on its own: while the
service runs, another systemctl start joins the current run. flock matters
when the same script also runs by hand, from cron or from another unit. The
schedule itself is covered in systemd-timers.
lsof also shows the child process: sleep inherited the descriptor from
flock and holds the lock too.
The lock file cannot be opened. A common case: student once ran the script by
hand, the file in /run/lock now belongs to student, and the root service gets
refused — in a shared sticky directory Ubuntu does not let root open another
user's file (fs.protected_regular = 2). flock exits with code 66:
Delete the file while nobody holds it and run the script as one user. A root
service can use a path of its own: /run/clean.lock.
That is how -n behaves: busy means code 1 and no message. To see skips in the
log, add || echo "skipped: the previous run is still going" >&2, or print a
message inside the script as in the recipe above.
flock could not find the command (code 69). Cron and services have a shorter
PATH than your terminal — use the full path: /usr/local/bin/clean.sh.
The runs lock different files, or one of them bypasses flock. Compare the
path everywhere: the crontab, ExecStart=, the script itself. /var/lock and
/run/lock are the same directory.
While the first copy holds the lock, rm removes the file from the directory.
The next flock creates a new file and happily locks that one — two copies run
side by side:
Never delete a lock file — neither by hand nor at the end of the script.
Every process the command starts inherits the descriptor. If the command leaves
a process running in the background, the lock lives as long as that process —
even after kill -9 of flock itself. The -o option closes the descriptor
before running the command, so only flock holds the lock.
set -e.[ -f lock ] does not prevent a second run.Found an inaccuracy? Report an error in the article