Skip to content
Infrastructure as Code iac state 5 min read

State Locking

Terraform state is a single source of truth that maps your configuration to real-world resources. When two people (or two CI jobs) run terraform apply against the same state at the same time, they can read stale data, overwrite each other’s writes, and leave the state file inconsistent with reality. State locking solves this by granting one writer exclusive access at a time, blocking everyone else until the operation finishes. This page explains why concurrent runs are dangerous, how each backend implements locking, and how to recover when a lock gets stuck.

Why concurrent applies corrupt state

A terraform apply is not atomic from the state’s perspective: Terraform reads the current state, computes a plan, mutates real infrastructure, then writes the new state back. If a second apply starts before the first one finishes its write, two bad things can happen:

  • Lost updates — both runs read the same starting state; the slower writer’s terraform.tfstate clobbers the faster one’s, so resources created by the first run become orphaned (no longer tracked).
  • Resource conflicts — both runs try to create or modify the same resource simultaneously, producing duplicate resources or provider API errors mid-apply.

Either outcome leaves the state file out of sync with real infrastructure, which is painful to reconcile by hand. Locking makes the read-plan-write cycle effectively serialized per state.

Locking protects a single state file. It does not coordinate across different states or workspaces — those are independent and can run in parallel safely.

How backends implement locking

Locking is a backend feature, not a core Terraform feature, so the mechanism depends on where your state lives. The local backend uses an OS-level file lock; remote backends use a purpose-built coordination primitive. Both Terraform 1.5+ and OpenTofu support the same backend locking semantics.

BackendLocking mechanismNotes
localOS advisory file lock (.terraform.tfstate.lock.info)Single-machine only; no cross-host protection
s3DynamoDB item (or native S3 lockfile, TF 1.10+)dynamodb_table historically required; use_lockfile is the modern path
azurermBlob leaseAutomatic; no extra resource to provision
gcsNative object lockingAutomatic
remote / cloud (HCP Terraform)Server-side run queueRuns are queued, not just locked

S3 with native lockfile

As of Terraform 1.10 (and OpenTofu 1.10), the S3 backend can lock using an S3 object instead of DynamoDB. This removes the extra table entirely.

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

S3 with DynamoDB (classic)

If you are on an older version or already run DynamoDB-based locking, the table needs a single LockID partition key:

resource "aws_dynamodb_table" "tf_locks" {
  name         = "acme-tf-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

terraform {
  backend "s3" {
    bucket         = "acme-tf-state"
    key            = "prod/network/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "acme-tf-locks"
  }
}

When an apply starts, the backend writes a lock item and removes it when the run ends.

Output:

Acquiring state lock. This may take a few moments...
aws_subnet.public[0]: Creating...
aws_subnet.public[0]: Creation complete after 2s [id=subnet-0a1b2c3d4e5f]
Releasing state lock. This may take a few moments...

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

What a lock collision looks like

If you start an operation while another holds the lock, Terraform refuses to proceed and prints the lock metadata:

Output:

Error: Error acquiring the state lock

Error message: ConditionalCheckFailedException: the conditional request failed
Lock Info:
  ID:        9f3c1a20-6b8e-4d2c-9a11-7e0c5b2d4f88
  Path:      acme-tf-state/prod/network/terraform.tfstate
  Operation: OperationTypeApply
  Who:       jdoe@build-runner-07
  Version:   1.10.3
  Created:   2026-06-14 09:42:11.402 +0000 UTC

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.

The Who, Created, and Operation fields tell you who holds the lock and why — check these before doing anything drastic. Most of the time the right move is simply to wait for the other run to finish.

Recovering a stuck lock with force-unlock

Sometimes a run is killed (CI timeout, crashed laptop, network drop) and never releases its lock. The lock then blocks every future operation. Use the lock ID from the error message to release it manually:

terraform force-unlock 9f3c1a20-6b8e-4d2c-9a11-7e0c5b2d4f88

Terraform asks for confirmation; pass -force to skip the prompt in automation.

Only force-unlock when you are certain no apply is still running. Unlocking an in-flight operation reintroduces exactly the corruption locking was meant to prevent. Verify with your CI dashboard or teammates first.

Disabling and tuning locking

You can opt out of locking per-command with -lock=false, and bound how long Terraform waits for a busy lock with -lock-timeout:

# Wait up to 5 minutes for a contended lock instead of failing immediately
terraform apply -lock-timeout=5m

# Skip locking entirely (dangerous — only for local experiments)
terraform plan -lock=false

-lock-timeout is the safe, recommended knob for CI: it lets queued jobs wait their turn instead of erroring out. -lock=false should be reserved for read-only local debugging where you accept the risk.

Best Practices

  • Always use a backend that supports locking for any shared or production state — never let teams collaborate on a local backend.
  • Prefer native locking (use_lockfile for S3, blob lease for Azure, server-side for HCP Terraform) to avoid maintaining extra infrastructure like DynamoDB.
  • Set -lock-timeout (e.g. 5m) in CI pipelines so concurrent jobs queue gracefully instead of failing on contention.
  • Inspect the lock metadata (Who, Created, Operation) before force-unlocking, and confirm no run is active.
  • Reserve terraform force-unlock for genuinely abandoned locks; treat it as a recovery tool, not a routine command.
  • Avoid -lock=false outside local, read-only experiments — disabling locking on shared state invites corruption.
Last updated June 14, 2026
Was this helpful?