Terraform Error Guide: 'Error acquiring the state lock' on plan/apply
Fix Terraform's 'Error acquiring the state lock': diagnose stale DynamoDB/blob locks, abandoned CI runs, expired credentials, and safely force-unlock state.
- #terraform
- #troubleshooting
- #errors
- #state
Stuck on this Terraform error? Get the free incident triage checklist
A one-page PDF — the exact steps to isolate, fix, and verify a production error like this one. No spam, unsubscribe anytime.
Guided workflow
Terraform: error acquiring the state lock
Something holds the lock. Whether that something still exists decides between waiting and force-unlocking — and getting it wrong can corrupt state.
- Diagnose
- Inspect
- Interpret
- Remediate
- Validate
- Roll back
- Prevent
0 of 7 steps done
-
The error is unusually informative: it names who took the lock, from where, when, and the operation. Do not skip past it — every subsequent decision depends on what it says.
terraform planRead-onlyWhat it means: Reproduce the error and read the ID, Who, Created and Operation fields. Note the age: a lock from four minutes ago and one from four days ago call for opposite responses.
-
This is the whole question. A lock held by a running apply is protecting you; a lock held by a job that died is just in the way.
- If: The lock names a CI job
- Look that job up. If it was cancelled, timed out, or the runner died, the lock is stale.
- If: The lock names a person
- Ask them. This takes a minute and removes all doubt — and they may be mid-apply.
- If: The lock is minutes old and names an operation in progress
- Wait. A long apply legitimately holds the lock for a long time.
- If: You cannot determine the holder at all
- Wait, or escalate. An unexplained lock is not evidence that nothing is running, and the downside of being wrong is much larger than the delay.
-
Force-unlocking does not stop the process holding the lock. If an apply really is still running, you now have two processes able to write one state file. Concurrent writes can corrupt state in ways that require restoring a backup — and a corrupted state file is a worse day than a delayed deploy, by a wide margin.
-
Only after you have positively established that the holding process no longer exists. Use the lock ID from the error verbatim — Terraform will refuse a mismatched ID, and that refusal is a safety feature rather than an obstacle to work around.
terraform force-unlock <LOCK_ID>DestructiveThis can destroy data or state. Read the interpretation below before running it.
Expect: Terraform confirms the lock was released.
What it means: This removes the lock record; it does NOT stop whatever holds it. If an apply is genuinely still running you now have two processes able to write one state file, and concurrent writes can corrupt state badly enough to need a backup restore. Safe only when the holder is confirmed gone — never because you are impatient, which is the exact situation in which it is dangerous.
-
A stale lock often follows an interrupted apply, which means state may be partially updated. Check before you change anything further.
terraform plan -refresh-onlyRead-onlyExpect: No changes, or a small, explicable set.
What it means: A large unexpected drift here means the interrupted run got further than you assumed. Reconcile that before running a normal plan, which would otherwise try to revert it.
terraform state list | wc -lRead-onlyWhat it means: Compare against what you expect. A sharp drop means state was damaged and should be restored from a backup rather than applied against.
-
Restore the previous state version from the backend rather than trying to repair it by hand. Most remote backends keep versions, and that is precisely the reason to use one — the recovery path exists before you need it. Identify the last version written by a completed apply, restore it, then run a refresh-only plan to see what reality has done since. Hand-editing state is a genuine last resort, and should always be done on a copy so the damaged original is still available if the edit makes things worse.
-
Set a CI job timeout shorter than your lock tolerance so a hung job fails visibly instead of holding the lock indefinitely. Ensure the pipeline releases the lock on cancellation. Use a backend with versioned state so a bad outcome is recoverable. And keep applies serialized per state file — concurrency here has no upside.
Saves to your Toolkit when signed in, and to this browser otherwise.
Overview
Terraform takes a lock on the state before it writes to it, so two concurrent runs cannot corrupt the same state file. When the backend already holds a lock — from a still-running apply, a crashed process, or a CI job that died mid-run — the new run cannot acquire it and aborts before doing any work.
You will see this on terraform plan, apply, or destroy:
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
Path: my-tf-state/prod/network/terraform.tfstate
Operation: OperationTypePlan
Who: runner@ci-build-4821
Version: 1.7.5
Created: 2026-06-23 13:41:02.118 +0000 UTC
Info:
The lock is held in the backend, not locally: an S3 backend uses a DynamoDB item, an azurerm backend uses a blob lease, and gcs uses an object generation. The Who, Created, and Operation fields tell you who holds it and how stale it is.
Symptoms
plan/apply/destroyfail immediately withError acquiring the state lock.- The
Lock Infoblock names another user, CI runner, or a run from minutes/hours ago. - A previous run was Ctrl-C’d, lost network, or the CI pod was evicted.
- Two pipelines target the same workspace at the same time.
terraform plan
Error: Error acquiring the state lock
Lock Info:
ID: 9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
Operation: OperationTypeApply
Who: jane@laptop
Created: 2026-06-23 13:12:44 +0000 UTC
Common Root Causes
1. A genuinely concurrent run holds the lock
Another engineer or pipeline is mid-apply against the same state right now. This is the lock working as designed — not an error to force past.
terraform plan 2>&1 | grep -A6 "Lock Info"
Lock Info:
Operation: OperationTypeApply
Who: deploy-bot@ci-build-4905
Created: 2026-06-23 14:02:55 +0000 UTC
A Created time of seconds ago plus OperationTypeApply means wait, don’t unlock.
2. A crashed or cancelled run left a stale lock
A Ctrl-C, evicted CI pod, or lost VPN during apply leaves the lock behind because Terraform never reached the unlock step.
aws dynamodb get-item \
--table-name terraform-locks \
--key '{"LockID":{"S":"my-tf-state/prod/network/terraform.tfstate"}}'
{
"Item": {
"LockID": {"S": "my-tf-state/prod/network/terraform.tfstate"},
"Info": {"S": "{\"ID\":\"9f1c4e2a-...\",\"Created\":\"2026-06-23T13:41:02Z\"}"}
}
}
If Created is hours old and no process is running, the lock is stale.
3. Two pipelines share one state/workspace
Separate CI jobs (e.g. PR validation and a merge deploy) both run against the same backend key with no serialization, so they race for the lock.
grep -A4 'backend "s3"' backend.tf
backend "s3" {
bucket = "my-tf-state"
key = "prod/network/terraform.tfstate"
dynamodb_table = "terraform-locks"
}
Two jobs with the same key will collide. Distinct workspaces or keys avoid it.
4. The lock table or lease backend is misconfigured
If dynamodb_table is missing on an S3 backend, locking is silently skipped — but on azurerm, a broken blob lease can leave a permanent lease that looks like a stuck lock.
az storage blob show \
--container-name tfstate --name prod.terraform.tfstate \
--account-name mytfstate --query "properties.lease" -o json
{
"state": "leased",
"status": "locked"
}
A leased/locked blob with no active run is a stale lease that must be broken.
5. Expired credentials abort apply mid-write
A short-lived STS/session token expires during a long apply, the process dies after locking but before unlocking, and the next run finds a stale lock.
aws sts get-caller-identity
An error occurred (ExpiredToken) when calling the GetCallerIdentity operation:
The security token included in the request is expired.
Refresh credentials first, otherwise even force-unlock may fail to reach the backend.
6. Clock skew or wrong backend key
Pointing at the wrong key/workspace shows a lock from an unrelated state, making it look held when your real state is fine.
terraform workspace show
terraform state list | head -1
default
Confirm you are operating on the state you think you are before unlocking anything.
Diagnostic Workflow
Step 1: Read the full lock info
terraform plan 2>&1 | sed -n '/Lock Info/,/^$/p'
Note Who, Operation, Created, and the lock ID — you need the ID to unlock.
Step 2: Decide if the run is live or dead
# CI: check whether the named runner/job is still active
gh run list --workflow deploy.yml --limit 5
If the Who job is still in_progress, wait. If it failed/cancelled, the lock is stale.
Step 3: Inspect the backend lock directly
# S3 + DynamoDB
aws dynamodb get-item --table-name terraform-locks \
--key '{"LockID":{"S":"<STATE_PATH>"}}'
# azurerm
az storage blob show --container-name tfstate --name <KEY> \
--account-name <ACCT> --query "properties.lease"
Confirm the lock’s age and that no process owns it.
Step 4: Refresh credentials
aws sts get-caller-identity || aws sso login
Make sure you can actually reach the backend before unlocking — a failed unlock can leave you worse off.
Step 5: Force-unlock with the exact lock ID, then retry
terraform force-unlock 9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
terraform plan
Only run force-unlock once you have confirmed no live run holds the lock.
Example Root Cause Analysis
A nightly deploy pipeline fails with Error acquiring the state lock on prod/network. The lock info reads:
Lock Info:
Operation: OperationTypeApply
Who: runner@ci-build-4821
Created: 2026-06-23 02:14:09 +0000 UTC
The named job ci-build-4821 is checked in CI and shows status cancelled — the runner was scaled down mid-apply at 02:14. No other run is active:
gh run list --workflow deploy.yml --limit 3
completed failure deploy ci-build-4905
cancelled cancelled deploy ci-build-4821
This is a stale lock from the cancelled job, not a live conflict. Credentials are valid:
aws sts get-caller-identity
{"Account": "123456789012", "Arn": "arn:aws:sts::123456789012:assumed-role/deploy/..."}
Fix: force-unlock the stale ID and re-run.
terraform force-unlock 9f1c4e2a-7b30-4a55-9d21-6f0e8b3c4d12
terraform apply -auto-approve
The apply proceeds normally. The deeper fix is to serialize the pipeline so a new run cannot start while the previous one is unfinished.
Prevention Best Practices
- Never
force-unlockreflexively. Confirm the namedWho/job is actually dead first — unlocking a live apply can corrupt state. - Serialize CI runs against each state with a concurrency group (e.g. one in-flight job per workspace) so pipelines never race for the lock.
- Use long-lived enough credentials (or auto-refresh) so a long apply cannot expire mid-run and orphan a lock.
- Set a backend timeout and prefer
terraform applyover Ctrl-C-prone interactive runs in automation; trapping signals to unlock cleanly helps. - Keep one
key/workspace per logical environment so unrelated runs never share a lock. - For triage, the free incident assistant can turn a lock-info block into a wait-or-unlock recommendation. More patterns in the Terraform guides.
Quick Command Reference
# See the full lock info
terraform plan 2>&1 | sed -n '/Lock Info/,/^$/p'
# Inspect the backend lock (S3 + DynamoDB)
aws dynamodb get-item --table-name terraform-locks \
--key '{"LockID":{"S":"<STATE_PATH>"}}'
# Inspect the lease (azurerm)
az storage blob show --container-name tfstate --name <KEY> \
--account-name <ACCT> --query "properties.lease"
# Confirm the named CI job is dead, not live
gh run list --workflow deploy.yml --limit 5
# Refresh credentials before unlocking
aws sts get-caller-identity || aws sso login
# Force-unlock with the exact ID (only when no live run holds it)
terraform force-unlock <LOCK_ID>
Conclusion
Error acquiring the state lock means the backend already holds a lock on your state. The usual root causes:
- A genuinely concurrent run holds it — wait, do not unlock.
- A crashed/cancelled run left a stale lock behind.
- Two pipelines share one
key/workspace and race for the lock. - A misconfigured lock table or stuck blob lease.
- Expired credentials killed an apply after it locked but before it unlocked.
- You are pointed at the wrong
key/workspace and seeing an unrelated lock.
Always read the lock info, confirm the holder is truly dead, then force-unlock with the exact ID. The durable fix is serializing runs so the lock is never abandoned.
Fixed it? Get 500 Terraform & DevOps AI prompts — free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.
Stuck on this? Start guided troubleshooting
Open an interactive diagnostic session with this error already loaded. Work a step-by-step plan, record what each check returns, land on a root cause, and export a clean incident summary — no account needed to start.
Did this fix your issue?
Solved it a different way?
Share the fix that worked for you — reviewed, then published to help the next engineer.
That looks like it may contain a secret (key, token, password, or connection string). Please remove it — a note with a detected secret can’t be published.
Thanks — that helps. Published notes appear after a quick review.
Trending errors this week
The error guides other engineers are actually reading right now.
- 1mount: wrong fs type, bad option, bad superblock
- 2mount: wrong fs type, bad option, bad superblock
- 3Kernel panic - not syncing: VFS: Unable to mount root fs on unknown-block
- 4gpg: keyserver receive failed: No data
- 5Docker 'failed to set up container networking': Fix the Bridge and IP Pool
- 6Docker 'failed to create shim task': How to Fix the containerd Runtime Error
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.