Skip to main content
Logo
Overview

Fix Terraform 'Error acquiring the state lock' Safely

June 10, 2026
9 min read

It’s almost always 2am when you see it. A CI job died halfway through an apply, and now every run greets you with the same wall of text:

Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional
request failed
Lock Info:
ID: a3f1c8e2-9b44-4d7e-8c11-2f6a9e0d5b3c
Path: my-tfstate-bucket/prod/terraform.tfstate
Operation: OperationTypeApply
Who: runner@ip-10-0-3-44
Version: 1.11.4
Created: 2026-06-10 01:58:12.4471 +0000 UTC

Nothing is broken. Terraform is doing exactly what it’s supposed to do — refusing to let two processes write the same state file at once. The problem is that the process holding the lock is already dead, and it never cleaned up after itself. So the lock just sits there, blocking everyone.

There’s a safe way out of this and an unsafe way, and the unsafe way can shred your state file. Let me walk through the whole thing — the emergency fix, the stale-DynamoDB case, and the 2026 shift to S3-native locking that makes half the old advice on this obsolete.

Read the lock block before you touch anything

The single most important habit: actually read the lock info in the error. It’s not boilerplate. Every field tells you whether this lock is safe to break.

  • ID — the lock ID you’ll feed to force-unlock. Copy it exactly.
  • Who — the user and host that grabbed the lock. If it says runner@ip-... and that runner is long gone, that’s your stale lock. If it says your coworker’s laptop and they’re online, go ask them before you do anything.
  • OperationOperationTypeApply is the scary one. A half-finished apply means resources may have changed but state didn’t get written back. OperationTypePlan is harmless to break.
  • Created — the timestamp. This is the field people skip, and it’s the one that matters most.

Look at that Created time and compare it to now. If the lock was created four hours ago and your applies normally take ninety seconds, it’s stale — the process is dead. If it was created forty seconds ago, slow down. Something might genuinely be running, and breaking a live lock is how state corruption happens.

The whole decision comes down to one question: is a real process still holding this lock? Not “is the lock annoying me,” but “is there an actual terraform apply alive somewhere writing to this state right now?” Everything below assumes you’ve answered no.

The safe path: terraform force-unlock

Once you’re confident the lock is stale, the correct tool is the one Terraform gives you:

terraform force-unlock a3f1c8e2-9b44-4d7e-8c11-2f6a9e0d5b3c

That’s it. Use the exact ID from the error block. Terraform will ask you to confirm, then release the lock through the backend — whether that’s DynamoDB, an S3 lockfile, or whatever else you’re using. This is the right answer maybe 90% of the time, and you should always try it before reaching for anything more invasive.

A couple of notes that trip people up. force-unlock needs to run against the same backend config and workspace as the locked state, so run it from the same directory, with the same terraform init already done. If you’re using workspaces, make sure you’re on the right one (terraform workspace show). The lock ID is workspace-specific, and unlocking the wrong workspace does nothing useful.

If force-unlock succeeds, run a terraform plan immediately. You want to see whether the dead apply left things in a weird state — resources created but not recorded, that kind of thing. Better to find out now than on the next deploy.

When force-unlock fails: clearing the lock from DynamoDB

Sometimes force-unlock itself errors out — usually a permissions problem, or the backend got into a state Terraform can’t reconcile. If you’re on the classic S3 + DynamoDB setup, you can delete the lock record directly.

First, understand what’s actually in that DynamoDB table, because this is where people cause real damage. The table holds two different kinds of items keyed by LockID:

  1. The lock itself: LockID = my-tfstate-bucket/prod/terraform.tfstate
  2. A digest item: LockID = my-tfstate-bucket/prod/terraform.tfstate-md5

That second one, ending in -md5, is not a lock. It’s the checksum Terraform uses to detect state tampering. Delete it and you’ll get “state data in S3 does not have the expected content” errors on your next run. Leave it alone. You only want to delete the first item — the one whose LockID is the bare state path with no suffix.

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

The LockID value is the S3 path to your state: <bucket>/<key>. It’s printed right there in the error block as Path:, so copy it from there rather than guessing. If you want to look before you leap, query the table first:

aws dynamodb scan --table-name terraform-locks

That shows you every lock and digest item so you can confirm you’re deleting the right one. After the delete, run terraform plan to confirm the backend is happy and nothing else is lingering.

I’ll be blunt: deleting from DynamoDB is the manual override, and manual overrides are where mistakes live. If force-unlock works, use it. Reach for delete-item only when Terraform genuinely can’t release the lock itself.

The 2026 shift: S3-native locking, no DynamoDB at all

Here’s the part most of the older guides haven’t caught up on. For years, “set up Terraform remote state” meant “create an S3 bucket and a DynamoDB table for locking.” That second table existed purely because S3 didn’t support the conditional writes Terraform needed to coordinate locks.

That’s no longer true. S3 added conditional write support, and Terraform 1.10 shipped an experimental use_lockfile option that locks state using S3 itself — a small .tflock file written next to your state with a conditional put. In Terraform 1.11, the feature graduated out of experimental, and the dynamodb_table, dynamodb_endpoint, and endpoints.dynamodb arguments were officially deprecated. HashiCorp’s guidance is now explicit: DynamoDB-based locking is discouraged and could be removed entirely in a future minor version. If you’re standing up new state today, there’s no reason to provision a lock table.

The config is almost anticlimactic:

terraform {
  backend "s3" {
    bucket       = "my-tfstate-bucket"
    key          = "prod/terraform.tfstate"
    region       = "us-east-1"
    encrypt      = true
    use_lockfile = true
  }
}

No dynamodb_table line. The lock now lives at prod/terraform.tfstate.tflock in the same bucket, created and deleted by Terraform around each operation. One bucket, one less piece of infrastructure to provision, pay for, and forget about.

Migrating an existing backend

If you already run DynamoDB locking, you don’t have to flip everything at once. You can set both during the transition:

terraform {
  backend "s3" {
    bucket         = "my-tfstate-bucket"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"   # keep during migration
    use_lockfile   = true                # add this
  }
}

With both set, Terraform writes to both lock mechanisms, so a teammate still on an older config or workflow won’t accidentally apply concurrently while you’re mid-migration. Roll use_lockfile = true out everywhere, confirm every pipeline and every engineer is on Terraform 1.11+, then drop the dynamodb_table line in a follow-up change and delete the table. Don’t delete the table the same day you add the lockfile — give the two-config period a beat so a straggler on the old setup doesn’t slip through.

One gotcha worth flagging: the IAM permissions change. DynamoDB locking needed dynamodb:GetItem, PutItem, and DeleteItem. S3-native locking instead needs s3:PutObject and s3:DeleteObject on the lockfile path (which you probably already grant for the state file), plus s3:GetObject. If your bucket policy was scoped tightly to a specific key, make sure it also covers the .tflock object, or your locks will fail to acquire with permission errors that look nothing like a stuck lock.

And to clear a stuck S3-native lock? Same as before — terraform force-unlock <ID> first. If that won’t budge it, the lock is just an object in your bucket, so you can aws s3 rm s3://my-tfstate-bucket/prod/terraform.tfstate.tflock as the manual override. Much harder to hurt yourself here than with DynamoDB, since there’s no digest item sitting next to it to delete by accident.

The one rule that keeps your state intact

Everything in this post hangs off a single principle, so let me state it plainly: never break a lock while a real apply is genuinely running.

If two terraform apply processes write the same state at the same time, you get a lost update at best and a corrupted, half-merged state file at worst — the kind of thing that takes an afternoon and a backup to recover from. The lock exists specifically to prevent that. When you force-unlock a live lock, you’re disabling the seatbelt mid-crash.

So before you unlock anything, confirm no live process holds it. Check the Who and Created fields. If it’s a CI runner, look at your CI dashboard — is that job still showing as running, or did it die? If it’s a person, message them. Thirty seconds of checking beats an hour of state surgery. When in doubt, wait a few minutes and re-run; a genuinely live operation will finish and release the lock on its own.

Stop creating stale locks in the first place

Clearing locks is treating the symptom. Almost every stale lock traces back to a process that died without cleanup, and you can cut the frequency way down with a few habits.

Set a lock timeout so Terraform retries instead of failing instantly when it hits a lock — useful when a previous step is just about to release:

terraform apply -lock-timeout=120s

That tells Terraform to keep retrying for two minutes before giving up, which smooths over the brief windows where one pipeline stage is finishing as another starts.

The bigger lever is your CI configuration. Most stale locks come from runners killed mid-apply — OOM kills, spot instance reclamation, job timeouts that fire during the apply step. Give your apply jobs enough memory headroom that they don’t get OOM-killed, avoid spot instances for the apply stage specifically, and set your CI job timeout longer than your longest realistic apply so the runner isn’t shot in the head halfway through. If you must use aggressive timeouts, at least make sure the lock gets released — but the cleaner fix is not killing the process to begin with.

And if you’re constantly fighting locks because five pipelines all target the same giant state file, that’s a signal to split your state. Smaller, well-bounded state files contend less, apply faster, and blast-radius less when something does go wrong.


Next time the error shows up, resist the urge to immediately Google “terraform unlock command” and paste the first thing you find. Read the lock block, decide if it’s stale, and if it is, force-unlock with the exact ID. That covers the vast majority of cases without ever touching DynamoDB. And if you’re still provisioning a lock table for new projects in 2026 — try use_lockfile = true on your next bucket and delete one moving part from your stack.

Sources: Terraform S3 backend docs, S3 native state locking writeup