---
title: "Bash PID File Locking: Stop Overlapping Script Runs"
lang: "en"
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/post/bash-script-locking-pid-file
---

![Blog post image for Bash Script Locking: Prevent Concurrent Runs with a PID File - A Bash snippet that uses a PID file so only one instance of a script runs at a time. Essential for cron jobs and automation scripts that must not overlap. Includes stale lock detection and cleanup on exit.](/_astro/hero.CF-paSMK_7v3YJ.webp)

[Home](/)›[Codesnippets](/codesnippets)›[All Categories](/codesnippets/categories)›[Bash](/codesnippets/categories/bash)

Codesnippets

[Bash](/codesnippets/categories/bash)[Automation](/codesnippets/categories/automation)[Devops](/codesnippets/categories/devops)[Bash](/codesnippets/bash)

# Bash Script Locking: Prevent Concurrent Runs with a PID File

[Mohammad Abu Mattar](/authors/mohammad-abu-mattar)Published: 18 Aug 2026Updated: 18 Aug 202604 Mins read04 Mins listen

[Markdown for AI(opens in a new tab)](/post/bash-script-locking-pid-file/index.md "Open the plain-Markdown version of this page, for pasting into an AI tool")

TL;DR

A Bash snippet that uses a PID file so only one instance of a script runs at a time. Essential for cron jobs and automation scripts that must not overlap. Includes stale lock detection and cleanup on exit.

Series

[Shell & Terminal Productivity](/series/shell--terminal-productivity)1/1

All posts in this series (1)

Code Snippets1

1.  [Bash Script Locking: Prevent Concurrent Runs with a PID FileYou are here](/codesnippets/post/bash-script-locking-pid-file)

### Bash Script Locking: Prevent Concurrent Runs with a PID File

Contents

[The Problem](#the-problem)[**The Problem**](#the-problem)[Real-world impact](#real-world-impact)[The Solution](#the-solution)[**The Fix**](#the-fix)[Script Implementation](#script-implementation)[Setup](#setup)[Acquiring the lock](#acquiring-the-lock)[Releasing on exit](#releasing-on-exit)[Entry point](#entry-point)[Usage and Benefits](#usage-and-benefits)[**Why This Helps**](#why-this-helps)[Running it](#running-it)[Community Discussion](#community-discussion)[**Your Turn**](#your-turn)[Alternative approaches](#alternative-approaches)

**Quick Tip**

Wrap any script that must not run twice at once in a PID-file lock, and a second copy simply exits instead of corrupting your data.

## [The Problem](#the-problem)

### [**The Problem**](#the-problem-1)

Cron does not care whether your last run finished. If a job is scheduled every five minutes but one run takes seven, cron cheerfully starts a second copy while the first is still going. Now two processes are reading the same input, writing the same output file, and racing each other to a half-written mess.

The same thing happens with a webhook that fires twice, a retry that overlaps the original, or an impatient human running the script by hand while the scheduled run is live.

### [Real-world impact](#real-world-impact)

Overlapping runs are the kind of bug that only shows up under load, which is exactly when you can least afford it. You get truncated files, doubled database rows, and log output interleaved from two processes so you cannot even tell what happened. The flow below shows what a single guarded run should do instead.

On startup the script checks for a PID file. If one exists and its process is alive, it exits. If the process is gone, the lock is stale and gets cleared. Either way it then writes its own PID and registers a trap to remove the file on exit.

## [The Solution](#the-solution)

### [**The Fix**](#the-fix)

Before doing any work, the script writes its own process ID into a lock file. If that file already exists, it checks whether the process it names is still alive. If it is, this run backs off and exits. If the process is gone, the lock is stale (a previous run crashed) so it clears it and carries on. A `trap` removes the lock file on the way out, whether the script finished cleanly or died.

**TL;DR**

-   One PID file per script decides who gets to run.
-   `kill -0 $pid` tells you if the previous owner is still alive without actually signalling it.
-   A `trap ... EXIT` guarantees the lock is released even on error.

When two runs overlap, the second one loses cleanly:

Cron fires the script twice. Run A creates the PID file and works. Run B sees the file, confirms A's process is still alive, and exits cleanly. When A finishes, its trap removes the lock so the next run can proceed.

## [Script Implementation](#script-implementation)

### [Setup](#setup)

Start strict, and decide where the lock lives. Naming it after the script keeps different jobs from stepping on each other’s locks.

with\_lock.sh

```
1#!/usr/bin/env bash2set -euo pipefail3
4# One lock per script name, in a writable runtime dir.5LOCK_FILE="${TMPDIR:-/tmp}/$(basename "$0").pid"
```

### [Acquiring the lock](#acquiring-the-lock)

This is the core. If a lock file exists and names a live process, exit. If it exists but the process is gone, treat it as stale and reclaim it.

Terminal window

```
1acquire_lock() {2  if [[ -e "$LOCK_FILE" ]]; then3    local old_pid4    old_pid=$(cat "$LOCK_FILE" 2>/dev/null || echo "")5
6    # kill -0 checks the process exists without sending a real signal.7    if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then8      echo "Already running as PID $old_pid, exiting." >&29      exit 110    fi11
12    # The owner is gone: stale lock from a crashed run. Reclaim it.13    echo "Clearing stale lock from PID ${old_pid:-unknown}." >&214  fi15
16  echo $$ > "$LOCK_FILE"17}
```

### [Releasing on exit](#releasing-on-exit)

Register the cleanup once, right after acquiring, so it runs on normal exit, on error (thanks to `set -e`), and on Ctrl-C.

Terminal window

```
1release_lock() {2  rm -f "$LOCK_FILE"3}4
5acquire_lock6trap release_lock EXIT
```

### [Entry point](#entry-point)

Everything after the trap is your real work. It runs knowing no other copy is active.

Terminal window

```
1main() {2  echo "Running with lock held (PID $$)..."3  # ... the actual job goes here ...4  sleep 55  echo "Done."6}7
8main "$@"
```

## [Usage and Benefits](#usage-and-benefits)

### [**Why This Helps**](#why-this-helps)

The whole guard is about fifteen lines and needs nothing outside of Bash itself. Drop it at the top of any script and overlapping runs stop being a problem: the first run works, the rest exit immediately, and a crash never leaves a lock that blocks every future run.

### [Running it](#running-it)

Prove it to yourself by starting one in the background and immediately launching a second:

Terminal window

```
1./with_lock.sh &   # first run grabs the lock2./with_lock.sh     # second run prints "Already running..." and exits 1
```

The first run holds the lock for its full duration; the second sees a live PID and steps aside. Kill the first mid-run and the next invocation clears the stale lock instead of getting stuck.

## [Community Discussion](#community-discussion)

### [**Your Turn**](#your-turn)

How do you keep your cron jobs from overlapping? PID files, lock directories, or something your scheduler does for you? I would like to hear what has held up in production.

### [Alternative approaches](#alternative-approaches)

If you are on Linux, `flock` is worth a look. It locks a file descriptor at the kernel level, so there is no stale-PID logic to write yourself:

Terminal window

```
1exec 9>"${TMPDIR:-/tmp}/$(basename "$0").lock"2flock -n 9 || { echo "Already running, exiting." >&2; exit 1; }3# lock is released automatically when fd 9 closes on exit
```

The PID-file version is more portable (it works the same on macOS and older shells) and it records who holds the lock, which is handy for debugging. `flock` is simpler and race-free where you have it. Pick whichever matches where your scripts run.

Was this useful?

## Tags

[#Bash](/codesnippets/tags/bash)[#Pid file](/codesnippets/tags/pid-file)[#Locking](/codesnippets/tags/locking)[#Flock](/codesnippets/tags/flock)[#Cron](/codesnippets/tags/cron)[#Concurrency](/codesnippets/tags/concurrency)

## Share

[Facebook](https://facebook.com/sharer/sharer.php?u=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file "Share on Facebook")[Twitter](https://twitter.com/intent/tweet/?text=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File&url=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file "Share on Twitter")[LinkedIn](https://www.linkedin.com/shareArticle?mini=true&url=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file&title=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File&summary=A%20Bash%20snippet%20that%20uses%20a%20PID%20file%20so%20only%20one%20instance%20of%20a%20script%20runs%20at%20a%20time.%20Essential%20for%20cron%20jobs%20and%20automation%20scripts%20that%20must%20not%20overlap.%20Includes%20stale%20lock%20detection%20and%20cleanup%20on%20exit.&source=https://mkabumattar.com "Share on LinkedIn")[WhatsApp](https://wa.me/?text=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File%20https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file "Share on WhatsApp")[Telegram](https://t.me/share/url?url=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file&text=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File "Share on Telegram")[Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file&title=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File "Share on Reddit")[Hacker News](http://news.ycombinator.com/submitlink?u=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file&t=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File "Share on Hacker News")[Pinterest](https://pinterest.com/pin/create/button/?url=https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file&media=&description=A%20Bash%20snippet%20that%20uses%20a%20PID%20file%20so%20only%20one%20instance%20of%20a%20script%20runs%20at%20a%20time.%20Essential%20for%20cron%20jobs%20and%20automation%20scripts%20that%20must%20not%20overlap.%20Includes%20stale%20lock%20detection%20and%20cleanup%20on%20exit. "Share on Pinterest")[Email](<mailto:?subject=Bash%20Script%20Locking%3A%20Prevent%20Concurrent%20Runs%20with%20a%20PID%20File&body=Check out this article: https%3A%2F%2Fmkabumattar.com%2Fcodesnippets%2Fpost%2Fbash-script-locking-pid-file>)

## Comments

## You might also enjoy

More posts on similar topics

[![Bash Retry Function: Automatically Retry Failing Commands with Exponential Backoff](/_astro/hero.Dap62rVN_Z1q0nar.webp)](/codesnippets/post/bash-retry-function-exponential-backoff)

## [Bash Retry Function: Automatically Retry Failing Commands with Exponential Backoff](/codesnippets/post/bash-retry-function-exponential-backoff)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Devops](/codesnippets/categories/devops)
-   [Shell scripting](/codesnippets/categories/shell-scripting)
-   [Automation](/codesnippets/categories/automation)

Quick Tip Wrap any flaky command in one reusable retry function and stop re-running red pipelines by hand. The Problem The Problem Some commands fail for reasons that have nothing to

[#Bash](/codesnippets/tags/bash)[#Retry](/codesnippets/tags/retry)[#Exponential Backoff](/codesnippets/tags/exponential-backoff)+3 tags

[read more](/codesnippets/post/bash-retry-function-exponential-backoff)

[![AWS EC2 Instance Management with Boto3: Start, Stop, and Query Instances](/_astro/hero.PnHlvJay_ZkFqWG.webp)](/codesnippets/post/aws-ec2-instance-management-boto3-python)

## [AWS EC2 Instance Management with Boto3: Start, Stop, and Query Instances](/codesnippets/post/aws-ec2-instance-management-boto3-python)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Cloud](/codesnippets/categories/cloud)
-   [Aws](/codesnippets/categories/aws)
-   [Devops](/codesnippets/categories/devops)
-   [Automation](/codesnippets/categories/automation)

If you've ever spent 20 minutes clicking through the AWS Console just to stop a handful of dev instances, you already know the pain. It's tedious, it doesn't scale, and one wrong click can ruin your a

[#AWS](/codesnippets/tags/aws)[#EC2](/codesnippets/tags/ec2)[#Boto3](/codesnippets/tags/boto3)+6 tags

[read more](/codesnippets/post/aws-ec2-instance-management-boto3-python)

[![Essential Bash Variables for Every Script](/_astro/hero.B4bDowyY_YQpD7.webp)](/codesnippets/post/essential-bash-variables)

## [Essential Bash Variables for Every Script](/codesnippets/post/essential-bash-variables)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Shell scripting](/codesnippets/categories/shell-scripting)
-   [Devops](/codesnippets/categories/devops)

Overview Quick Tip You know what's worse than writing scripts? Writing scripts that break every time you move them to a different machine. Built-in Bash variables fix that. The problem wi

[#Bash](/codesnippets/tags/bash)[#Shell Scripting](/codesnippets/tags/shell-scripting)[#Linux](/codesnippets/tags/linux)+4 tags

[read more](/codesnippets/post/essential-bash-variables)

[![Multi-Environment Secret Management with HashiCorp Vault](/_astro/hero.B4H3tk7I_1BuPOM.webp)](/codesnippets/post/hashicorp-vault-multi-environment-secrets)

## [Multi-Environment Secret Management with HashiCorp Vault](/codesnippets/post/hashicorp-vault-multi-environment-secrets)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Security](/codesnippets/categories/security)
-   [Devops](/codesnippets/categories/devops)

Managing secrets safely across multiple environments with HashiCorp Vault Storing secrets in .env files, hardcoding them, or even using separate secret managers per environment creates security

[#Vault](/codesnippets/tags/vault)[#SecretsManagement](/codesnippets/tags/secretsmanagement)[#MultiEnvironment](/codesnippets/tags/multienvironment)+3 tags

[read more](/codesnippets/post/hashicorp-vault-multi-environment-secrets)

[![Check S3 Bucket Existence](/_astro/hero.CWiO4GWV_1YUYTb.webp)](/codesnippets/post/bash-s3-bucket-exists)

## [Check S3 Bucket Existence](/codesnippets/post/bash-s3-bucket-exists)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Aws](/codesnippets/categories/aws)
-   [Shell scripting](/codesnippets/categories/shell-scripting)
-   [Devops](/codesnippets/categories/devops)

Quick Tip Don't let your deployment blow up because of a missing S3 bucket. This Bash script lets you check if a bucket exists before anything fails. The Problem Missing bucket failure

[#Bash](/codesnippets/tags/bash)[#AWS](/codesnippets/tags/aws)[#DevOps](/codesnippets/tags/devops)+3 tags

[read more](/codesnippets/post/bash-s3-bucket-exists)

[![AWS Secrets Manager](/_astro/hero.BIDl4oI2_Z1yJEXf.webp)](/codesnippets/post/nodejs-aws-secrets-manager)

## [AWS Secrets Manager](/codesnippets/post/nodejs-aws-secrets-manager)

-   [Mohammad Abu Mattar](/authors/mohammad-abu-mattar)
-   [Aws](/codesnippets/categories/aws)
-   [Devops](/codesnippets/categories/devops)

Loading secrets in a Node.js app without exposing them If you're still storing API keys or database credentials in .env files or hardcoding them into your codebase, it's time for a better appro

[#NodeJS](/codesnippets/tags/nodejs)[#TypeScript](/codesnippets/tags/typescript)[#AWS](/codesnippets/tags/aws)+3 tags

[read more](/codesnippets/post/nodejs-aws-secrets-manager)

6 related posts
