OpenTofu Error Guide: 'Error acquiring the state lock' — Release a Stuck Lock
Fix OpenTofu's 'Error acquiring the state lock' error: understand DynamoDB and backend locks, safely force-unlock an abandoned lock, and stop CI runs from deadlocking your state.
- #iac
- #infrastructure-as-code
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
OpenTofu locks your state file whenever it runs an operation that could write to it — plan with a refresh, apply, destroy, state commands. The lock prevents two runs from mutating the same state at once and corrupting it. When OpenTofu cannot acquire that lock, it refuses to proceed and prints the lock details.
The error looks like this (an S3 backend with DynamoDB locking shown; other backends print the same top line):
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 3f5b1c9a-8e21-4d77-9b0e-2a1f6c4d8e90
Path: my-tofu-state/prod/terraform.tfstate
Operation: OperationTypeApply
Who: runner@ci-agent-7
Version: 1.8.0
Created: 2026-07-06 14:22:10.334 +0000 UTC
Info:
The lock is doing its job: something holds it and OpenTofu will not risk a concurrent write. The important distinction is whether the lock is live (another run is genuinely in progress — wait) or stale (a previous run crashed, timed out, or was killed and never released it — force-unlock). Force-unlocking a live lock can corrupt state, so the whole workflow is about proving the holder is dead before you break the lock.
Symptoms
tofu plan/tofu applyfails immediately withError acquiring the state lock.- The
Lock Infoblock names aWho,Createdtime, andID. - CI jobs hang or fail because a previous job on the same state never released its lock.
- The named holder (a pod, CI runner, or laptop) is no longer running.
tofu apply
Error: Error acquiring the state lock
Lock Info:
ID: 3f5b1c9a-8e21-4d77-9b0e-2a1f6c4d8e90
Who: runner@ci-agent-7
Created: 2026-07-06 14:22:10 +0000 UTC
Common Root Causes
1. A previous run was killed and never released the lock
The most common cause. A CI job hit its timeout, a runner was reclaimed, or someone pressed Ctrl-C twice during apply. OpenTofu never reached the “release lock” step, so the lock persists.
Who: runner@ci-agent-7
Created: 2026-07-06 14:22:10 +0000 UTC # created hours ago; that runner is gone
2. A genuinely concurrent run (the lock is live)
Two pipelines, or a teammate and CI, targeted the same state at the same time. This is the lock working correctly — one run must wait, not force-unlock.
# Check whether a run is actually in progress in your CI system before touching the lock.
3. Backend / lock-table connectivity problems
For an S3 + DynamoDB backend, a throttled or unreachable lock table makes acquiring (or releasing) the lock fail. The error may read as a lock failure when the real issue is IAM or connectivity.
Error: Error acquiring the state lock
Error message: AccessDeniedException: User is not authorized to perform:
dynamodb:PutItem on resource: .../tofu-locks
4. Wrong or missing lock configuration
A backend block that omits the lock table (or points at the wrong one) can cause intermittent lock errors or, worse, no locking at all. Confirm the backend config matches across all runners.
5. A leftover lock from a crashed local run
On a local or file/GCS/Azure backend, an interrupted run can leave a lock entry that no live process owns.
Diagnostic Workflow
Step 1: Read the full lock info
tofu apply 2>&1 | sed -n '/Lock Info/,/Info:/p'
Note the ID (needed to force-unlock), the Who (which host/runner holds it), and Created (how old it is). A lock created seconds ago by a running job is live; one created hours ago by a dead runner is stale.
Step 2: Prove the holder is actually dead
This is the critical safety step. Confirm the Who is not running before you break the lock:
# If the holder is a CI runner, check the job in your CI system (is it still running?).
# If it is a Kubernetes pod / Atlantis, check it directly:
kubectl get pods -A | grep ci-agent-7
# If it is a person's laptop, ask them. Do not force-unlock a live run.
Step 3: Inspect the lock at the backend (optional, S3+DynamoDB)
You can read the lock item directly to confirm it is stale:
aws dynamodb get-item \
--table-name tofu-locks \
--key '{"LockID":{"S":"my-tofu-state/prod/terraform.tfstate"}}'
{ "Item": { "LockID": {"S": "..."}, "Info": {"S": "{\"Who\":\"runner@ci-agent-7\",...}"} } }
An item present here with a holder you have confirmed is dead is safe to release.
Step 4: Force-unlock with the exact lock ID
Only after confirming the holder is dead, release the lock using the ID from the error:
tofu force-unlock 3f5b1c9a-8e21-4d77-9b0e-2a1f6c4d8e90
Do you really want to force-unlock?
OpenTofu will remove the lock on the remote state.
This will allow local OpenTofu commands to modify this state, even though it
may be still be in use. Only 'yes' will be accepted to confirm.
Enter a value: yes
OpenTofu state has been successfully unlocked!
Never pass -force in automation to skip the prompt unless you have already proven the lock is stale.
Step 5: Verify state integrity before applying
A killed run may have left state mid-write. Confirm it is readable and sane before the next apply:
tofu state pull > /tmp/state-check.json
jq '.serial, (.resources | length)' /tmp/state-check.json
tofu plan
If state pull fails or the plan shows unexpected mass changes, restore from a backend version/backup rather than applying.
Example Root Cause Analysis
A nightly tofu apply pipeline on the prod state began failing every run with Error acquiring the state lock, holder runner@ci-agent-7, created at 14:22 the previous afternoon.
The team first checked whether a run was genuinely in progress — the concurrent-run case — before touching anything:
kubectl get pods -A | grep ci-agent-7
(no output — the runner pod no longer exists)
The CI job history showed that yesterday’s ci-agent-7 job had been evicted mid-apply when its node was drained for maintenance. OpenTofu never ran its lock-release step, so the DynamoDB lock item persisted and blocked every subsequent run.
With the holder confirmed dead, the lock was released using the exact ID from the error:
tofu force-unlock 3f5b1c9a-8e21-4d77-9b0e-2a1f6c4d8e90
OpenTofu state has been successfully unlocked!
State was then pulled and planned to confirm the interrupted apply had not left a half-written state:
tofu state pull | jq '.serial'
tofu plan
128
No changes. Your infrastructure matches the configuration.
The serial was intact and the plan was clean, so the pipeline resumed normally. To stop recurrence, the CI job was given a graceful-shutdown grace period and a lock-timeout so an evicted run releases or fails fast instead of stranding the lock.
Prevention Best Practices
- Never
kill -9anapply; let it finish or interrupt once and wait for it to release the lock cleanly. See our infrastructure-as-code guides. - Serialize state access per environment in CI (concurrency groups / job queues) so two runs never target the same state at once.
- Set a
-lock-timeout(e.g.tofu apply -lock-timeout=120s) so a briefly-held lock is waited out instead of failing instantly. - Give CI runners a shutdown grace period so an evicted job can release its lock before the pod is killed.
- For S3+DynamoDB backends, confirm the lock table exists, is reachable, and the runner’s IAM role can
PutItem/DeleteItemon it; a permission gap masquerades as a lock error. - Enable state versioning on the backend (S3 bucket versioning, etc.) so you can restore if a killed run left corrupt state.
Quick Command Reference
# See the full lock details from the error
tofu apply 2>&1 | sed -n '/Lock Info/,/Info:/p'
# Confirm the holder (Who) is not still running before unlocking
kubectl get pods -A | grep RUNNER_NAME
# Inspect the lock item directly (S3 + DynamoDB backend)
aws dynamodb get-item --table-name tofu-locks \
--key '{"LockID":{"S":"PATH/terraform.tfstate"}}'
# Release a stale lock using the exact ID from the error
tofu force-unlock LOCK_ID
# Wait for a briefly-held lock instead of failing immediately
tofu apply -lock-timeout=120s
# Verify state is intact after a killed run
tofu state pull | jq '.serial, (.resources | length)'
tofu plan
Conclusion
Error acquiring the state lock means something already holds the lock on your OpenTofu state. The usual root causes:
- A previous run was killed and never released the lock (most common — stale lock).
- A genuinely concurrent run holds the lock (live — wait, do not force-unlock).
- Backend or lock-table connectivity/IAM problems.
- Wrong or missing lock configuration across runners.
- A leftover lock from a crashed local run.
Read the lock ID and holder, prove the holder is dead, then tofu force-unlock <ID> — and verify state with state pull and plan before applying. Prevent recurrence with per-environment serialization, lock timeouts, graceful CI shutdowns, and state versioning so an interrupted run never strands your team.
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.