Skip to main content

Terraform · troubleshooting

Error acquiring the state lock

Something holds the lock. Usually a run that was killed before it could release — but possibly a run that is still very much alive, and `force-unlock` on that one can corrupt your state. The whole question is which.

Run this first

step 1 of 3
# Read the whole Info block from the error, not just the first line.

Look for `Who` and `Created`. A recent timestamp and a runner you can still find means wait; an old timestamp and a job that ended abnormally means clear it.

The lock metadata answers the only question that matters: is the holder alive? Everything else follows from that, and skipping it is how state gets corrupted.

Work out which cause you have

A few questions to narrow the list. Every answer ends in a command that confirms or rules the cause out — this cannot see your cluster, so nothing here is a certainty until you have checked.

Narrow it down

0 answered · nothing is sent anywhere

Does the error's Info block name a Who and a Created time?

If those fields are absent, nobody holds the lock and this is a different problem.

Cause space

4 of 4 still possible

  • A previous run was killed before it released the lockCommon
  • The run holding it is still goingCommon
  • Two pipelines are racing for the same stateCommon
  • The backend cannot be written, so the lock cannot be takenOccasional

Nothing ruled out yet. Answer the question above and the branches your answer eliminates will strike through here.

Check it with a tool

Or diagnose it manually

In this order. The first command usually contains the whole answer.

  1. Step 1

    # Read the whole Info block from the error, not just the first line.

    The lock metadata answers the only question that matters: is the holder alive? Everything else follows from that, and skipping it is how state gets corrupted.

    Look for `Who` and `Created`. A recent timestamp and a runner you can still find means wait; an old timestamp and a job that ended abnormally means clear it.

  2. Step 2

    # Check whether the job or terminal named in Who is still running.

    This is the step people skip, and it is the one that separates a safe unlock from a destructive one.

    Look for A live process or job. If you find one, stop — the lock is working and waiting is correct.

  3. Step 3

    terraform plan -lock=false

    A read-only way to see whether the backend itself is reachable, without taking a lock. Safe for a plan and never for an apply.

    Look for A plan that succeeds means the backend is fine and the lock is genuinely held. An error means the backend is the problem, so this is a permissions issue rather than a lock.

Every cause, and how to fix it

Ordered by how often each one turns out to be the answer.

A previous run was killed before it released the lock

Common

Ctrl-C, a CI job timeout, a runner scaled away, a lost network connection, a laptop closed. Terraform tries to release on interrupt, but a hard kill leaves no opportunity — so the lock persists with the dead run's metadata still attached.

Confirm

# Read the Created and Who from the error, then check that run.
# For CI, find the job that matches and confirm how it ended.

A creation time matching a run you know ended abnormally, and a `Who` naming a machine or job that is no longer running. Both together make this safe to clear.

Fix

  • Confirm the holder is genuinely gone before doing anything else. That means checking the actual job or machine, not inferring from elapsed time.
  • Then release it with the lock ID from the error message. Passing the ID rather than using a blanket unlock is deliberate — it fails if the lock has changed hands since you read the error.
terraform force-unlock <LOCK_ID>

Only after confirming the holder is dead. If you are guessing, you are choosing between a blocked pipeline and corrupted state, and the first is much cheaper.

The run holding it is still going

Common

A long apply — a database, a cluster, anything with a slow create — can easily outlast somebody's patience. The lock is working correctly, and the second run is being told to wait rather than being blocked by a fault.

Confirm

# Compare the lock's Created timestamp against your longest plausible apply.
# Then check the CI job or terminal named in Who.

A currently-running job matching `Who`. If you find one, stop — waiting is the correct action and force-unlock here is the failure mode this lock exists to prevent.

Fix

  • Wait. Terraform will also retry with `-lock-timeout` if you would rather it block than fail.
  • If long applies routinely collide, the fix is splitting state so unrelated changes do not serialise behind each other — not shortening the lock.
terraform apply -lock-timeout=10m

Waits for the lock instead of failing immediately. Usually what you wanted.

Two pipelines are racing for the same state

Common

Two merges close together, or a manual run alongside an automated one. The lock is doing exactly what it should, and the error is a symptom of missing concurrency control one level up.

Confirm

# Look for overlapping runs against the same backend key in your CI history.

Two jobs whose windows overlap and target the same state path. If the `Who` names a CI runner, this is the likely cause.

Fix

  • Serialise the pipeline for a given state — most CI systems have a concurrency group for exactly this.
  • Do not solve it by increasing the lock timeout. That makes the collisions invisible rather than absent, and the queue can grow faster than it drains.

The backend cannot be written, so the lock cannot be taken

Occasional

Distinguishable from a held lock and often confused with it. An IAM change removing write access to the lock table, a missing table, or a KMS key the caller can no longer use produces an acquisition failure with no holder — the lock was never taken rather than being taken by somebody else.

Confirm

aws dynamodb describe-table --table-name <lock-table> >/dev/null && echo 'table reachable'

An access-denied or not-found error rather than a lock-held error. If the error names no `Who` and no `Created`, nobody holds it — this is a permissions problem.

Fix

  • Check the caller's permissions on the lock table and the state bucket, including the KMS key if the state is encrypted.
  • `force-unlock` cannot help here and will fail for the same reason the lock could not be taken.

Understanding it properly

Skip this if you are mid-incident — the working part of the page is above. Worth reading afterwards, because understanding the mechanism is what stops the next one.

What is actually happening

Terraform takes a lock before writing state so that two runs cannot read the same state, each decide what to change, and then overwrite one another. Without it, concurrent applies silently lose changes.

The lock is held in the backend — a DynamoDB item for S3, a blob lease for Azure, a row for Postgres — and is released when the run finishes. If the process dies first, nothing releases it, and the lock outlives the run that took it.

So the error is doing its job. The dangerous part is the obvious fix: `force-unlock` deletes the lock regardless of whether the holder is finished. Run it while another apply is genuinely in progress and you have removed the only thing preventing two writers.

The error message includes the lock's metadata — who took it, when, from where, and the operation. That is enough to answer the only question that matters before you break it.

How to tell this is your problem
WhereWhat you see
terraform plan / apply`Error acquiring the state lock` followed by an Info block naming ID, Path, Operation, Who, Created.
AWS S3 backend`ConditionalCheckFailedException` from DynamoDB — the conditional write that implements the lock.
CI logsA previous run that ended in a cancellation or timeout rather than a completion.

How to know it is actually fixed

  • `terraform plan` acquires the lock and completes.
  • The lock is released afterwards — a second plan should not be blocked.
  • If you force-unlocked, run a plan before any apply and read it carefully. An empty diff is the reassurance you want; unexpected changes mean the state may have been written by the run you interrupted.

Stopping it happening again

  • Set `-lock-timeout` in CI so a run waits rather than failing on a transient collision.
  • Add a concurrency group so only one run per state can be in flight at a time. This removes the most common cause outright.
  • Split large state. Unrelated changes serialising behind each other is the underlying reason collisions are frequent, and smaller state also shortens every plan.
  • Treat `force-unlock` as an operation requiring a check, not a reflex. The habit of confirming the holder is what stops this becoming a corruption incident.

Did this get you to an answer?

No text box on purpose — please do not paste production logs anywhere

Sources

Behaviour described here is drawn from official documentation. Where a figure could not be confirmed on an official page it is attributed in the text rather than stated as canonical.

Related

Error acquiring the state lock — causes, diagnosis and fix | DevOps Insights