</>CodeWithKarani

Error Acquiring the State Lock: When Terraform force-unlock Is Safe

Karani GeoffreyKarani Geoffrey8 min read

Someone cancels a pipeline. Ten minutes later the next apply fails, then the one after that, then the deploy for a completely unrelated service that happens to share the state. Within half an hour there is a Slack thread with six people in it and someone has already pasted terraform force-unlock with a shrug emoji.

That command is not a "clear the error" button. It is you signing a statement that says: I have checked, and no other process is currently writing this state. If that statement is false, Terraform does exactly what you told it and lets a second writer in alongside the first. Nothing errors. You find out later, when a plan wants to destroy production because half of it disappeared from state.

So the interesting question is never "how do I force-unlock". It is "how do I know it is safe to". That takes about ninety seconds, and this is how you spend them.

read the Who and Created fields in the Lock Info block, then open your CI run history and confirm that run is finished or cancelled. Only then run terraform force-unlock <LOCK_ID> using the exact ID from the error. If a run really is still in flight, wait: forcing it produces two concurrent writers, which is far worse than five minutes of waiting. And never use -lock=false to get past this.

The exact error: Error acquiring the state lock

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
  ID:        a1b2c3d4-e5f6-7890-abcd-ef1234567890
  Path:      my-bucket/prod/terraform.tfstate
  Operation: OperationTypeApply
  Who:       runner@fv-az1024-321
  Version:   1.9.8
  Created:   2026-03-19 14:23:01.123456 +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=false"
flag, but this is not recommended.

Everything you need to make the decision is in that block, and most people scroll straight past it to the ID.

  • Who is user@hostname from the machine that took the lock. A CI runner hostname here (runner@fv-az..., root@runner-xyz) tells you immediately this is a pipeline lock, not a colleague at a laptop.
  • Created is when the lock was taken. Compare it to how long your longest apply legitimately takes. Two minutes old is probably a live run. Nine hours old is a corpse.
  • Operation distinguishes OperationTypePlan from OperationTypeApply. A dead plan lock is harmless to clear. A dead apply lock means resources may have been created that never made it into state.

Why a cancelled CI job leaves the lock behind

Terraform releases the lock as part of shutting down cleanly. It also handles an interrupt: the first Ctrl+C asks it to stop gracefully, finish what it is doing and unlock. What it cannot handle is SIGKILL, and that is precisely what a cancelled or timed-out CI job eventually sends. The process disappears mid-flight, and the lock is just a row in a table or an object in a bucket that nobody deleted.

This is worth being precise about, because the lock is the symptom and rarely the real damage:

What a cancelled apply actually leaves behind 14:23:01 terraform apply starts, lock written to DynamoDB or as a .tflock object 14:26:40 8 resources created in the cloud provider, still only in memory 14:31:00 Pipeline cancelled. SIGKILL. No graceful shutdown, no cleanup step. 14:31:00 State never uploaded. Lock never released. 14:44:12 Next run: state lock error, plus 8 real resources nobody tracks.
Force-unlock fixes the last line only. The orphaned resources are still there, and they are the expensive half of the problem.

Where the lock actually lives, by backend

You cannot inspect or clean up a lock you cannot find, and it is different for every backend:

BackendWhere the lock livesManual inspection
S3 with use_lockfile = trueAn object next to the state, key + .tflockaws s3api head-object
S3 with dynamodb_table (deprecated)Item in the table, LockID = bucket/keyaws dynamodb get-item
azurermBlob lease on the state blobaz storage blob show, lease state
gcsLock object in the same bucketgsutil ls
HCP Terraform / Terraform CloudWorkspace-level lockWorkspace UI has an unlock action
GitLab-managed state (http)Held by GitLab for that state nameInfrastructure > Terraform states page

One change worth knowing if you are on the S3 backend: use_lockfile now does locking natively through S3 conditional writes, and dynamodb_table and dynamodb_endpoint are deprecated and slated for removal. If you are still running a lock table purely for locking, that is a migration you can do in an afternoon and it removes an entire class of "the table got deleted" incidents.

The decision, in the right order

Error acquiring the state lock Read Lock Info: ID, Who, Created, Operation Open CI run history. Is that run still executing? yes, or unsure no, it was killed Wait. Do not force-unlock. Forcing now creates two concurrent writers. terraform force-unlock <LOCK_ID> Use the exact ID from the error, nothing else. Then plan, and hunt for resources missing from state
The only branch that matters is the middle one. Everything after it is mechanical.

Step 1: Confirm nothing is actually running

Not "ask in Slack and wait ten seconds". Look at the run history in GitHub Actions, GitLab CI, Jenkins, Atlantis, whatever executes your Terraform, and find the run whose start time matches Created and whose runner matches Who. You want to see it in a terminal state: cancelled, failed, timed out.

If the state is shared by more than one pipeline, check them all. This is the moment where "I only looked at the pipeline I was running" becomes a state corruption incident.

Step 2: Force-unlock with the exact ID

terraform force-unlock a1b2c3d4-e5f6-7890-abcd-ef1234567890

Terraform prints a warning and asks you to type yes. Read the warning rather than reflexively confirming; it is showing you the same lock info again as a last chance. On success you get a short confirmation that the state has been unlocked, and nothing else happens: force-unlock never touches your infrastructure or your state contents.

The command takes the lock ID as a positional argument and has one flag, -force, which only skips the confirmation prompt. There is no "unlock everything" mode, and that is deliberate.

Step 3: When force-unlock itself fails

If the backend is unreachable, or the lock ID no longer matches what is stored, you can go to the storage layer directly. For the S3 native lock:

aws s3api head-object --bucket my-bucket --key prod/terraform.tfstate.tflock
aws s3api delete-object --bucket my-bucket --key prod/terraform.tfstate.tflock

For a DynamoDB lock table:

aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-bucket/prod/terraform.tfstate"}}'

aws dynamodb delete-item \
  --table-name terraform-locks \
  --key '{"LockID": {"S": "my-bucket/prod/terraform.tfstate"}}'

Delete only the lock item. The same table usually also holds an item whose LockID ends in -md5. That one is not a lock, it is the checksum Terraform uses to detect a stale read from S3. Delete it and your next run will refuse to proceed because the state in S3 does not match the digest it expected, and now you have a second, more confusing problem on top of the first.

Step 4: Reconcile, because the lock was never the real issue

terraform plan -detailed-exitcode

If the killed run was an apply, this is the important step. Exit code 0 means no changes and you are clean. Exit code 2 means there is a diff, and you now need to read it properly: a plan that wants to create resources you can see already existing in the console means the apply created them and died before writing state. Those need terraform import, or deleting by hand, before anything else happens on this stack.

Verification

terraform plan

A clean plan that gets past the refresh stage without a lock error is the proof. If you want to check the lock is genuinely gone at the storage layer, aws s3api head-object on the .tflock key should return a 404, or get-item on the DynamoDB key should return an empty response rather than an item.

One more check worth doing after any forced unlock on a versioned bucket: list the state object versions and confirm the most recent one has a sensible timestamp and size. A state file that suddenly shrank by 90% is the signature of a split-brain write, and the fix is restoring the previous object version, which is the same reflex as keeping a real point-in-time recovery path for a database.

What people get wrong

Using -lock=false. This is the worst thing in this article. It does not clear the stale lock, it tells Terraform to ignore locking entirely for this run, which means you will happily write state on top of whatever the other process is doing. The error text itself says it is not recommended, and it is in there for backends that genuinely cannot lock, not as an escape hatch.

Putting force-unlock in the pipeline as a pre-step. I have seen this "fix" more than once: a step that unlocks before every apply so the job never gets blocked. That deletes your concurrency protection completely. The whole point of the lock is to stop two pipeline runs from writing at once, and this guarantees they can.

Deleting the DynamoDB table or emptying it. Beyond the -md5 problem above, you are also blowing away locks for every other state that shares the table. Someone else's live apply is now unprotected.

Assuming force-unlock rolls something back. It does not. It removes a lock record. Any resources created before the process was killed are still there, still billing, and now invisible to Terraform.

Retrying the pipeline until it works. If a run really is in flight, retrying just queues up more contenders, and whichever one wins will be operating on a state that is about to be overwritten by the run you did not wait for.

When it is still broken

  1. The lock reappears within seconds of unlocking. Something is genuinely running. Look for a scheduled pipeline, an Atlantis autoplan, or a drift-detection job you forgot about.
  2. Terraform complains the state does not match the expected digest. You (or someone) removed the -md5 item, or an S3 write is still settling. Wait a couple of minutes and retry before touching anything; if it persists, restore the state object from bucket versioning.
  3. Force-unlock says the lock ID does not match. The stored lock is from a different operation than the error you are reading, usually because a second run took the lock in between. Re-run the command that failed to get a fresh error and a fresh ID.
  4. It happens every week. Then fix the cause, not the symptom: give the job a timeout longer than your slowest apply, make cancellation send a graceful signal with a grace period rather than an immediate kill, split one giant state into per-environment states so a stuck lock does not block everyone, and move to S3 native locking with bucket versioning on.

The general lesson is the same one as reading the conditions on a Kubernetes HPA: the tool already told you what it knows. The Lock Info block has the who, the when and the what. Ninety seconds of reading it is the difference between an inconvenience and a state corruption postmortem.

Reference: terraform force-unlock documentation and the S3 backend documentation.

Frequently asked questions

Is it safe to run terraform force-unlock?
It is safe only after you have confirmed no other Terraform process is writing that state. Read the Who and Created fields in the Lock Info block, then find the matching run in your CI history and verify it is cancelled, failed or finished. If a run is genuinely still executing, force-unlocking lets a second process write state concurrently, which can corrupt it.
Why does my Terraform state stay locked after I cancel a CI job?
Terraform releases the lock during a clean shutdown and on a graceful interrupt, but a cancelled or timed-out CI job usually ends with SIGKILL, which the process cannot trap. The lock record is left behind in DynamoDB, as a .tflock object, or as a blob lease, depending on your backend, and nothing removes it automatically.
Should I just use terraform apply -lock=false to get past a state lock error?
No. That flag does not clear the stale lock, it disables locking for the run entirely, so if another process is mid-apply you will both write state and one set of changes will be lost. Terraform's own error text calls it not recommended. Use force-unlock with the specific lock ID after verifying nothing else is running.
Can I delete the lock directly from the DynamoDB table?
Yes, delete the item whose LockID is bucket-name/path/to/terraform.tfstate, but only that item. The table also holds an item whose LockID ends in -md5, which is the state checksum Terraform uses to detect stale reads from S3. Deleting that one causes Terraform to refuse the next run because the state does not match the digest it expected.
#Terraform#State Management#CI/CD#AWS#Infrastructure as Code
Keep reading

Related articles