Recovering from a Stuck Terraform State Lock
terraform plan or apply hangs, then fails with 'Error acquiring the state lock.' Here's how to confirm it's genuinely stale before force-unlocking it.
terraform plan or terraform apply fails with Error acquiring the state lock when the configured backend’s locking mechanism believes another operation holds the state. The implementation is backend-specific. For the S3 backend, native lock files are current and DynamoDB-based locking is deprecated; HCP Terraform and other backends manage locks differently.
Step 1: don’t force-unlock immediately — check if it’s genuinely stale first
The error output itself includes useful context:
Error: Error acquiring the state lock
Lock Info:
ID: 7c1b3f2e-...
Path: prod/terraform.tfstate
Operation: OperationTypeApply
Who: ci-runner-42@github-actions
Created: 2025-10-20 14:02:33 UTC
Check whether the process or CI run identified in Who and Created is actually still running. If a CI pipeline named in the lock is genuinely still executing (check your CI system’s dashboard directly), the lock is legitimate — waiting for it to finish is the correct action, not force-unlocking underneath it.
Step 2: confirm the holding process is actually dead
If the Created timestamp is old relative to how long that kind of operation normally takes, and the named CI run/process shows as finished, crashed, or cancelled in your CI system — not currently executing — the lock is stale, left behind by a process that didn’t clean up after itself (a killed pipeline, a network partition during apply, a manually interrupted terraform apply).
Step 3: force-unlock, once you’re confident it’s stale
terraform force-unlock 7c1b3f2e-...
This requires the lock ID shown in the error output — Terraform deliberately requires this explicit ID rather than a blanket “unlock everything” command, specifically to make you look at and confirm the correct lock before removing it.
Run it from the same initialized configuration, workspace, backend key, and credentials that produced the error. Prefer the interactive confirmation; -force suppresses a useful safety prompt and belongs only in a controlled automation procedure. Some local-state locks cannot be unlocked by this command.
Step 4: after unlocking, check for actual state corruption
A lock left behind by a crashed or killed apply sometimes means that operation was interrupted mid-write to the state file itself, not just holding the lock idly. After force-unlocking, run a plan and inspect it carefully before applying anything:
terraform plan
If the plan shows unexpected, large-scale changes (resources it thinks need recreating that you didn’t touch), this can indicate the interrupted operation left state in a partially-updated condition — compare against your remote backend’s state file versioning (S3 versioning, Terraform Cloud’s state history) to check whether a slightly earlier state version is actually more accurate.
Step 5: recovering from a truly corrupted state file
If the state is genuinely inconsistent with reality, terraform state subcommands let you inspect and repair specific resources without needing to restore an entire backup:
terraform state list
terraform state show aws_instance.web
terraform plan -refresh-only
terraform apply -refresh-only
Review a refresh-only plan before applying it. Refresh-only mode updates state and root outputs to match remote objects without changing those objects, but it can still record destructive external drift. The older terraform refresh command is deprecated because it effectively auto-approves a refresh and provides less opportunity to review.
Treat state restoration as a controlled recovery
Do not assume an interrupted apply corrupted state merely because a stale lock remains. Modern remote backends use controlled writes, and restoring an older state can make Terraform forget resources created successfully. Compare state versions, provider records, cloud audit logs, and real resources before choosing a backup. Preserve the current state as evidence and restrict backend access throughout recovery.
After unlocking, run terraform plan -lock-timeout=... rather than disabling locking. Explain every proposed action and import or remove individual bindings only when the real ownership is known. Never run two recovery applies concurrently.
Prevent recurrence by serializing each state key in CI, allowing cancellation to terminate gracefully, using realistic timeouts, and enabling backend versioning. For S3, use use_lockfile = true, bucket versioning, encryption, least-privilege access, and documented recovery. Monitor abandoned jobs and lock age, but require human confirmation before automated force-unlock.
Preventing this going forward
The most effective prevention is making sure CI pipelines running terraform apply have reasonable timeouts and handle cancellation gracefully (running with a lock-release step even on failure/cancellation), and that remote state backends have versioning enabled (S3 bucket versioning, in particular) so a bad state write is always recoverable by reverting to a prior version — rather than relying on locks alone to prevent every possible interruption scenario.
Why locking exists in the first place
State locking exists specifically to prevent two concurrent apply operations from writing to the same state file at once, which would corrupt Terraform’s record of what it actually manages. A stuck lock is locking doing exactly what it’s supposed to do — flag a potential conflict — the work here is confirming whether that flagged conflict is real (another operation genuinely in progress) or stale (a crashed process that never released it), not treating every lock error as something to immediately force through.
Related:
- Infrastructure as Code: Terraform State, Drift, and Idempotency
- Terraform State in Production: Backends, Locking, Drift, Import, and Recovery
Sources: