Terraform locks state before any operation that could write to it, so two applies cannot run at once and corrupt each other. This error means the lock is already held.
The lock is doing its job far more often than it is broken. Before reaching for force-unlock, work out which situation you are in, because breaking a live lock is how state actually gets corrupted.
Read the lock info first
Terraform prints everything you need:
Lock Info:
ID: 8f3c1e2a-4b7d-11ef-9c2a-0242ac120002
Operation: OperationTypeApply
Who: alice@laptop
Created: 2026-09-16 09:14:22.481 +0000 UTC
Three fields decide what to do:
| Field | What it tells you |
|---|---|
Who | Whose machine or which CI runner holds it |
Created | How long it has been held |
Operation | OperationTypeApply is far more serious than OperationTypePlan |
A lock created two minutes ago by a colleague running an apply is not stuck. Wait.
A lock created three hours ago by a CI runner that no longer exists is stuck, and safe to break once you have confirmed nothing is running.
When it is genuinely a live lock
Someone or something is mid-operation. Options, in order:
Wait. Most applies finish in minutes.
Wait automatically, which is the right default in CI:
terraform apply -lock-timeout=10m
Terraform retries for that long before failing. Two pipelines that would otherwise collide simply queue.
Ask the person named in Who. Obvious, and routinely skipped.
When the lock is genuinely stuck
A process killed mid-operation: a cancelled CI job, a closed laptop, a network drop, Ctrl-C at the wrong moment. Leaves the lock behind with nothing holding it.
Before unlocking, establish that nothing is running. Specifically:
- Check your CI system for a running job on that workspace.
- Ask the person in
Whowhether they are mid-apply. - If the timestamp is minutes old and you cannot account for it, wait rather than guess.
Then:
terraform force-unlock 8f3c1e2a-4b7d-11ef-9c2a-0242ac120002
The ID comes from the error message. Terraform asks for confirmation, and -force skips the prompt, which you should not use interactively.
Why breaking a live lock is dangerous
If an apply genuinely is running and you force-unlock, a second apply can start against the same state. Both write to it, and the one that finishes last overwrites the other's record.
The result is state that does not match reality: resources exist that Terraform does not know about, and resources in state that were never created. Recovering means importing and removing entries by hand, and on a large configuration that is a genuinely bad day.
This is the one Terraform operation where being slow is correct.
Recovering if state is already corrupted
Enable versioning on your state bucket before you need it, that is the difference between a five-minute recovery and a rebuild.
aws s3api list-object-versions \
--bucket acme-tfstate \
--prefix production/terraform.tfstate \
--query 'Versions[:5].[VersionId,LastModified,Size]' --output table
aws s3api get-object \
--bucket acme-tfstate \
--key production/terraform.tfstate \
--version-id <good-version> \
restored.tfstate
terraform state push restored.tfstate
Then run a plan and read it extremely carefully before applying anything.
Backend-specific notes
S3 with native locking: modern Terraform supports use_lockfile = true, which stores a lock object in S3 itself and removes the DynamoDB requirement:
terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "production/terraform.tfstate"
region = "eu-west-1"
encrypt = true
use_lockfile = true
}
}
S3 with DynamoDB: the older pattern. ConditionalCheckFailedException in the error means the lock row already exists. You can see it directly:
aws dynamodb get-item \
--table-name terraform-locks \
--key '{"LockID":{"S":"acme-tfstate/production/terraform.tfstate"}}'
Deleting that row by hand works and is strictly worse than force-unlock, which performs the same operation with a confirmation prompt and correct handling of the digest entry.
Terraform Cloud / HCP: locks are managed in the UI, and a run can be discarded there. force-unlock does not apply.
Stopping it recurring
| Measure | Effect |
|---|---|
-lock-timeout=10m in CI | Concurrent pipelines queue instead of failing |
| Pipeline concurrency limits | Only one run per workspace at a time |
| Separate state per environment | A staging run cannot block production |
| Bucket versioning | Recovery is possible if a lock is broken badly |
Avoid Ctrl-C during apply | Let it finish, or expect a stuck lock |
The concurrency control is the highest-value one. A pipeline configured so two merges cannot apply the same workspace simultaneously removes the common cause entirely, and queueing at the CI layer gives a clearer outcome than a lock timeout.
A checklist
- Read
Who,CreatedandOperationfrom the lock info. - Created minutes ago by a real person or a running job → wait.
- Check CI for a running job on this workspace.
- Ask the person named in
Who. - Only once you are certain nothing is running:
terraform force-unlock <ID>. - Run a plan afterwards and read it before applying.
- Add
-lock-timeoutand pipeline concurrency limits so it stops happening.
Frequently Asked Questions
What does "Error acquiring the state lock" mean?
Terraform locks the state file before any operation that could write to it, so two runs cannot modify it simultaneously and corrupt each other. This error means something already holds that lock, usually a colleague or a CI job that is genuinely mid-operation, and sometimes a process that died and left the lock behind. The lock info Terraform prints tells you which: check who holds it, when it was created, and what operation it was performing.
Is terraform force-unlock safe?
Only when you have confirmed nothing is actually running. Breaking a live lock allows a second apply to start against the same state, and whichever finishes last overwrites the other's record. Leaving state that does not match reality, with resources missing from it and entries for things that were never created. Recovering means importing and removing state entries by hand. Check your CI system and ask whoever is named in the lock before running it.
How do I find the lock ID for force-unlock?
Terraform prints it in the error message under Lock Info as ID. Copy that value into terraform force-unlock <ID>. If you have lost the output, running any Terraform command against the workspace produces the error again with the same ID. For a DynamoDB backend you can also read the lock row directly with aws dynamodb get-item, though force-unlock is the correct tool because it handles the digest entry properly.
How do I prevent state lock errors in CI/CD?
Set -lock-timeout=10m on plan and apply so a run waits for the lock rather than failing immediately, which makes concurrent pipelines queue. Add concurrency controls at the CI layer so only one run per workspace executes at a time, which is the more reliable fix since queueing there gives a clearer outcome than a timeout. Keeping separate state per environment also helps, since a staging run then cannot block a production one.
Can I just delete the DynamoDB lock row?
It works, and terraform force-unlock is better. The command does the same thing with a confirmation prompt, validates the lock ID you supply against the one actually held, and handles the accompanying digest entry correctly. Deleting rows by hand risks removing the wrong one or leaving the digest inconsistent. Either way, the important step is the same: confirm nothing is genuinely running first.
Do I still need a DynamoDB table for Terraform state locking?
Not on recent versions. Terraform now supports native S3 locking via use_lockfile = true in the backend block, which stores a lock object alongside the state and removes the separate table entirely. One less resource to provision, pay for and manage. The DynamoDB pattern remains supported and is what older configurations use, so you will encounter both. If you are setting up a new backend, prefer the native option.