Skip to main content
DevOps Insightsguides & answers

Terraform · troubleshooting

Error acquiring the state lock

Something holds the Terraform state lock, usually a run that was killed before it could release it. But it may be a run that is still very much alive, and force-unlock on that one can corrupt your state. The whole question is which.

Reproduced on Terraform 1.14.0, . Where the text says “reproduced”, the behaviour was run on that version, not taken from documentation.

First published

New to this? Read what is actually happening first, then come back to the steps.

On this page · 10 sections

Before you start

  • A terminal in the same working directory and workspace as the run that failed, with terraform init done, so every command targets the same state.
  • A way to see whether the run named in Who is still alive: your CI system's job list, or access to the machine.
  • Read access to the backend that stores the state. Taking or releasing a lock on S3 also needs s3:GetObject, s3:PutObject and s3:DeleteObject on the <key>.tflock object; with the deprecated DynamoDB locking, dynamodb:GetItem, dynamodb:PutItem and dynamodb:DeleteItem on the table.
  • Agreement from whoever owns the run before you clear a lock that is not yours.

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 Lock 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 plan that does not take the lock, to see whether the backend itself is reachable. A plan changes nothing, which makes this the low-risk test, but Terraform's documentation still calls running without the lock dangerous when others might be running against the same workspace. Never use it with apply.

    Look for A plan that succeeds means the state itself is readable. It does not exercise the lock, so it cannot rule out missing write permission on the lock table or lock object; check the original error for AccessDenied. 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 gives it no chance, so on a remote backend the lock persists with the dead run's metadata still attached. Local state is different: the operating system drops the lock with the process. Reproduced with Terraform 1.14.0: after kill -9 on the holder, the next plan ran at once, though a stale .terraform.tfstate.lock.info file was left behind.

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. The ID is required, and on S3 Terraform checks it against the stored lock and refuses if they differ, so a lock that has changed hands since you read the error is not removed.
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. On the pg backend this does not work: the lock is a Postgres advisory lock held by the other session, released only when that session ends (find it in pg_locks). With local state, force-unlock from another terminal cannot clear a lock held by a live process: it reports Failed to unlock state: LocalState not locked (reproduced).

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

# Read the AccessDenied or NotFound text in Terraform's own error: it names the denied action and resource.
# DynamoDB locking (deprecated): check GetItem, PutItem and DeleteItem, not just DescribeTable.
# use_lockfile: check s3:GetObject, s3:PutObject and s3:DeleteObject on <key>.tflock.

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.

In plain terms

Terraform keeps a file, called state, that records what it has built. Before any run that could change that file, Terraform puts a lock on it so that two runs cannot edit it at once. This error means another run holds the lock, or held it and never let go, and the whole job is finding out which before you touch anything.

The collision, reproduced

Reproduced with Terraform 1.14.0 and the default local backend: one terminal ran terraform apply on a resource that takes 40 seconds, and a second terminal ran terraform plan four seconds later. Only Who is changed, to a placeholder; it showed the real user and machine name.

Command

terraform plan

Output

Error: Error acquiring the state lock

Error message: resource temporarily unavailable
Lock Info:
  ID:        0ea174f8-5845-dcdd-7730-4181f010bb10
  Path:      terraform.tfstate
  Operation: OperationTypeApply
  Who:       <user>@<host>
  Version:   1.14.0
  Created:   2026-10-12 07:01:59.649868 +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.

How to read it

  • Error message is the backend's own reason. With local state it is the operating system's file-lock error; remote backends word it differently.
  • Operation: OperationTypeApply says the holder is an apply, the case where forcing the lock open does the most damage.
  • Who and Created identify the holder. Created is in UTC, so convert it before comparing it with your CI's timestamps.
  • Run the same command with -lock-timeout=60s and it prints Acquiring state lock. This may take a few moments..., waits, and completes once the holder finishes: 47 seconds later in this reproduction.

Words used on this page

State
Terraform's record of the real resources it manages, by default the file terraform.tfstate. Terraform compares your configuration, this record and the real infrastructure to work out a plan.
Backend
Where state is kept: a local file by default, or a shared store such as S3, Azure Blob Storage, Google Cloud Storage or Postgres, so a team and its CI work from one copy.
State lock
A marker Terraform takes in the backend before any operation that could write state. A second run that finds it stops, or waits if you gave it -lock-timeout.
Lock Info
The block printed under the error: ID, Path, Operation, Who (user@host), Version, Created (in UTC) and Info.
Lock ID
The ID in Lock Info. terraform force-unlock needs it, and on S3 Terraform refuses to unlock if it no longer matches the stored lock.
force-unlock
A command that removes the lock without changing any infrastructure. Safe only when the run that took the lock is gone.
-lock-timeout
An option that makes a run keep retrying for the lock for a set time, such as -lock-timeout=60s, instead of failing at once.
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 lives in the backend: a <key>.tflock object for S3 with use_lockfile = true (DynamoDB-table locking still works but is deprecated), an infinite blob lease for Azure, a session-level advisory lock for Postgres, and an operating-system file lock for local state. The run releases it when it finishes. If the process dies first, it depends on that choice: a lock object or a lease stays until someone removes it, while a local file lock and a Postgres advisory lock are released by the operating system or the database when the process or connection ends.

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.

On backends that store lock metadata (S3, local state and others), the error's Lock Info block holds the lock's metadata — who took it, when, from where, and the operation. The pg backend is an exception: its Lock Info block describes your own blocked run, and the holder is found in the pg_locks view. 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 / applyError acquiring the state lock, then a Lock Info: block naming ID, Path, Operation, Who, Version and Created.
AWS S3 backendWith the deprecated dynamodb_table locking: ConditionalCheckFailedException from DynamoDB, the conditional write that implements the lock. With use_lockfile, the lock is a .tflock object in the state bucket.
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.

Security considerations

  • The Lock Info block names a user, a machine and the state's path. Redact Who and Path before pasting the error into a public issue or forum.
  • Never pass -lock=false to apply. Terraform's documentation calls running without the lock dangerous whenever others might run commands against the same workspace, and an apply is where two writers corrupt state.
  • Releasing your own lock and forcing someone else's need the same backend permission, so permissions cannot stop a careless force-unlock. Limit who can run Terraform against shared state at all, for example to the CI role and on-call, and make checking the holder a written step in the runbook.
  • State is stored as plain text and includes any secret values in your configuration. Keep it in a backend with encryption at rest and access control, and use ephemeral values or write-only arguments to keep secrets out of state altogether.

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.

Terms used here (1)

Short definitions for the vocabulary on this page, in case any of it is new. Each links to the full entry.

Terraform
An Infrastructure as Code tool by HashiCorp that lets you define and provision cloud and on-premises infrastructure using declarative configuration files (HCL). Since version 1.6 it has been source-available under the Business Source License rather than open source; OpenTofu is the open-source fork. Terraform manages the full lifecycle of resources through providers, plugins for cloud platforms and other APIs that number in the thousands.

All 59 terms in the glossary

Did this get you to an answer?

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

Something wrong or out of date? Report it