A stuck state lock stops every apply cold, and the fix is one command away once you confirm nobody else is actually running Terraform.
You run terraform apply, and instead of a plan you get a wall of red telling you the state lock could not be acquired. Nothing is broken, and your infrastructure is fine. Terraform is refusing to proceed on purpose, because it thinks another operation already owns the state. Most of the time the truth is simpler: a previous run died and never cleaned up after itself.
Terraform stores the mapping between your configuration and real-world resources in a state file. Any operation that could write to that state (apply, destroy, state mv, import) first takes a lock on the backend so that only one writer runs at a time.
Without locking, two people running apply against the same state at the same moment can interleave their writes and leave the state file describing resources that do not match reality. That is one of the hardest failure modes to recover from in Terraform, so locking is a deliberate guardrail, not an inconvenience. The lock is acquired at the start of the operation and released when it finishes cleanly.
The message is verbose on purpose. It hands you everything you need to decide what to do next:
$ terraform apply
Acquiring state lock. This may take a few moments...
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: d8f3c1a2-9b7e-4f6a-8c21-2a5f0e9d7b41
Path: my-tf-state/prod/terraform.tfstate
Operation: OperationTypeApply
Who: kiril@build-agent-07
Version: 1.9.5
Created: 2026-07-29 14:02:11.884213 +0000 UTC
Info:
Terraform acquires a state lock to protect the state from being written
by multiple users at the same time. Please resolve the issue above and try
again. For most commands, you can disable locking with the "-lock" flag
if you're certain that only one copy of Terraform is running.
Read the Lock Info block before doing anything else. Who tells you which machine and user holds the lock. Created tells you how old it is. Operation tells you what they were doing. If Who is your own crashed CI runner from twenty minutes ago and no job is currently active, you have a stale lock. If Who is a colleague and the timestamp is thirty seconds old, someone is actively running Terraform and you should wait.
There are three that account for nearly everything you will see:
A previous run crashed or was interrupted: Someone hit Ctrl-C mid-apply, a laptop went to sleep, or a CI job hit its timeout and got killed. Terraform never reached the release step, so the lock sits there orphaned.
A genuine concurrent run: Two pipelines triggered off the same branch, or a teammate is applying by hand while CI does the same. This is the case the lock is designed to catch. The fix is patience, not force.
A network blip to the backend: The lock was written, then the connection to the backend dropped before the release could complete. The lock record persists even though nothing is running.
The order of operations matters more than the command itself.
Who and Created fields against reality.terraform force-unlock d8f3c1a2-9b7e-4f6a-8c21-2a5f0e9d7b41
Terraform will ask for confirmation and then remove the lock record. Your next apply will acquire a fresh lock normally. Always pass the exact ID from the message; do not guess.
force-unlock does exactly what it says. It removes the lock regardless of whether the holder is still working. If you unlock a lock that a live apply genuinely owns, you have just invited a second writer into the state, and you are back to the corruption scenario locking was built to prevent. Recovering from a mangled state file can mean hand-editing JSON, re-importing resources, or restoring a backend version, all under pressure.
Treat force-unlock as a deliberate act. The verification step is the important part; the command is trivial. Never wire force-unlock into an automatic retry or a pipeline step that runs unattended.
How the lock is stored depends on your backend, and the last-resort cleanup differs.
S3 with a DynamoDB lock table (the classic): The state lives in S3, and the lock is a single item in a DynamoDB table you point at with dynamodb_table. Newer Terraform and OpenTofu versions also support S3-native locking via use_lockfile, but the DynamoDB pattern is still everywhere. When force-unlock cannot reach the backend, you can inspect the lock item directly:
aws dynamodb get-item \
--table-name terraform-locks \
--key '{"LockID": {"S": "my-tf-state/prod/terraform.tfstate-md5"}}'
# Last resort, only after confirming no run is active:
aws dynamodb delete-item \
--table-name terraform-locks \
--key '{"LockID": {"S": "my-tf-state/prod/terraform.tfstate-md5"}}'
The LockID value is the state path with a suffix; copy it from the get-item output rather than constructing it by hand.
Terraform Cloud / HCP: Locking is managed by the platform per workspace. There is no lock table to poke at. Unlock from the workspace UI (Settings, then the lock control) or with terraform force-unlock against the remote backend. A workspace can also be locked deliberately by a human, which shows up the same way, so check who locked it before overriding.
GCS: The lock is held in the same Cloud Storage bucket as the state, so there is no separate table. force-unlock is the right tool; a leftover lock object in the bucket can be removed manually only if the API path fails.
azurerm: The backend uses an Azure Blob lease on the state blob. A stuck lease can be broken through force-unlock, or as a last resort by breaking the blob lease from the Azure portal or CLI. Breaking a live lease has the same corruption risk as any force-unlock.
You can make this a rare event rather than a weekly ritual.
Serialize CI: Use your platform's concurrency controls so only one Terraform job per state runs at a time. GitHub Actions concurrency groups, GitLab resource_group, and similar features queue runs instead of colliding.
Do not Ctrl-C mid-apply: If a run is slow, let it finish or let it time out gracefully. Killing the process is the single most common way to orphan a lock.
Set a lock timeout: Instead of failing instantly on a busy lock, let Terraform wait and retry:
terraform apply -lock-timeout=120s
This smooths over short overlaps and network blips without any manual intervention, and it turns many transient lock errors into a brief pause.
For the broader catalog of failures you will hit with plans, providers, and modules, keep the Terraform error troubleshooting guide handy.
Treat a lock error as a question, not an emergency. Read the Lock Info, confirm from your CI dashboard and your team that nothing is actually running, then force-unlock with the exact ID. Reach for backend-level deletion only when force-unlock itself cannot talk to the backend. Then spend ten minutes adding a concurrency group and a -lock-timeout so the same interruption does not page you again next week.
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
A service mesh solves real problems and creates new ones. This is the map: what it actually does, when it earns its cost, and how the options compare.
A "Cycle" error means two Terraform resources depend on each other; here is how to find the loop and break it cleanly.
Explore more articles in this category
Terraform's errors are scarier than the fixes. This is the map to the ones everyone hits: what each message means, the safe way out, and how to avoid losing state.
A practical guide to renaming resources, migrating backends, and splitting or merging Terraform state without destroying and recreating infrastructure.
A practical guide to resolving Terraform provider version conflicts and lock file checksum errors across modules, developers, and CI pipelines.
Evergreen posts worth revisiting.