Terraform

Terraform State Locking: What It Is and Why It Matters

AGAnurag Gupta05 Apr 2025 Β· Updated 04 Oct 2026 Β· 7 min read
Terraform State Locking: What It Is and Why It Matters

Quick answer: Terraform state locking is a mutual-exclusion mechanism that lets only one plan, apply or state-modifying command run against a given state file at a time. Before touching state, Terraform acquires a lock in the backend (a DynamoDB item, an S3 lock object, an Azure blob lease, a GCS lock file or an HCP Terraform run); anyone else who tries gets an “Error acquiring the state lock” message until the first run finishes. It matters because two concurrent writes to state silently lose changes and orphan real cloud resources.

Locking is the least glamorous Terraform feature and one of the most important. This guide explains the race condition it prevents, how locking works in each popular backend, how to configure it correctly on S3 (including the new native lock file), what to do when you hit a stale lock, when force-unlock is safe, and the locking mistakes that teams make in CI pipelines.

The problem locking solves

Imagine two engineers, Asha and Rohit, both running terraform apply on the same project within the same minute, with no locking:

  1. Both read the current state, serial = 41.
  2. Asha’s apply creates a security group and writes state serial = 42 containing it.
  3. Rohit’s apply, still working from the old in-memory state, creates an IAM role and writes serial = 42 as well, overwriting Asha’s file.

Asha’s security group now exists in AWS but appears in nobody’s state. The next plan wants to create it again and fails with “already exists”. Multiply this by a CI system that triggers an apply on every merge and you have a daily source of mystery drift, with resources whose dependencies and ordering (see Understanding Resource Dependencies and Ordering in Terraform) are no longer tracked anywhere. Locking makes step 3 impossible: Rohit’s command waits (or fails fast) until Asha’s lock is released.

Locking only exists for remote backends, which is one more reason the local backend is unsuitable for teams, as covered in Understanding Terraform State: What It Is and Why It Matters.

How a lock works under the hood

When you run any command that may write state, Terraform:

  1. Writes a small lock record to the backend containing a lock ID (a UUID), the operation (OperationTypeApply), who is running it, the machine name, a timestamp and the Terraform version.
  2. Performs the plan or apply.
  3. Deletes the lock record.

If step 1 fails because a record already exists, Terraform prints the existing lock’s metadata and exits. That metadata is what you read to decide whether a lock is genuinely held or left over from a crashed run. Read-only commands such as terraform state list and terraform output do not take a lock.

Locking support by backend

Backend Lock mechanism Extra setup needed?
s3 (Terraform 1.10+) .tflock object written with S3 conditional writes No; set use_lockfile = true
s3 (classic) Item in a DynamoDB table keyed by LockID Yes; create the table and set dynamodb_table
azurerm Blob lease on the state blob No
gcs .tflock object in the bucket No
remote / cloud (HCP Terraform) Workspace lock; runs are queued No
consul Consul session lock; lock = true by default No
pg PostgreSQL advisory lock No
local OS file lock on the same machine only Not usable across machines

Older articles describe Consul and DynamoDB as the two locking options. Today every mainstream backend locks, and the S3 backend no longer needs DynamoDB at all.

Configuring locking on S3: native lock file vs. DynamoDB

The modern approach, available since Terraform 1.10, needs one extra line:

terraform {
  required_version = ">= 1.10.0"

  backend "s3" {
    bucket       = "tkh-terraform-state-123456789012"
    key          = "payments/prod/terraform.tfstate"
    region       = "ap-south-1"
    encrypt      = true
    use_lockfile = true
  }
}

Terraform writes payments/prod/terraform.tfstate.tflock next to the state using an S3 conditional PUT, which fails atomically if the object already exists. The IAM role running Terraform needs s3:PutObject, s3:GetObject and s3:DeleteObject on that path.

If your team is on an older Terraform or has an existing DynamoDB table, the classic configuration still works and can even be combined with the lock file during a transition:

terraform {
  backend "s3" {
    bucket         = "tkh-terraform-state-123456789012"
    key            = "payments/prod/terraform.tfstate"
    region         = "ap-south-1"
    encrypt        = true
    dynamodb_table = "terraform-state-locks"   # table with partition key "LockID" (String)
    use_lockfile   = true                      # optional: lock in both during migration
  }
}

Create the DynamoDB table with on-demand billing and a single string partition key named exactly LockID. Its cost is a few rupees a month. For the complete backend setup, including encryption and versioning, see Storing Terraform State Remotely: Benefits and Best Practices.

Handling a stuck lock

Locks are released automatically on success, failure or Ctrl+C. They are not released if the process is killed with SIGKILL, the machine loses power, or a CI runner is terminated mid-apply. The next run then fails:

$ terraform plan
Error: Error acquiring the state lock

Error message: operation error S3: PutObject, ... PreconditionFailed
Lock Info:
  ID:        7d1c4a1e-2b7f-4f3c-9a2e-1f0b6c9d8e55
  Path:      tkh-terraform-state-123456789012/payments/prod/terraform.tfstate
  Operation: OperationTypeApply
  Who:       ci-runner@gha-runner-42
  Version:   1.10.3
  Created:   2025-11-04 09:12:31 UTC

Before doing anything, check the Who and Created fields. If a colleague or a pipeline is genuinely applying right now, wait. If the run died twenty minutes ago and nothing is still executing, release the lock with the ID shown:

terraform force-unlock 7d1c4a1e-2b7f-4f3c-9a2e-1f0b6c9d8e55

Terraform asks for confirmation. The lock ID requirement is a safety feature: you cannot blindly unlock without first reading who holds it. Never automate force-unlock in a pipeline; a human should always confirm that the previous run is truly dead.

For transient contention, for example several pipelines queuing on one state, use -lock-timeout so Terraform retries instead of failing instantly:

terraform apply -lock-timeout=10m -auto-approve tfplan

Six locking mistakes to avoid

  1. Running with -lock=false to “fix” a stuck lock. You have just recreated the race condition the lock prevents. Diagnose and force-unlock instead.
  2. Automating force-unlock in CI. If a pipeline unlocks whatever it finds, a slow but healthy apply gets trampled. Keep it manual.
  3. Forgetting dynamodb_table or use_lockfile on S3. Unlike Azure and GCS, S3 does not lock unless you ask. Audit every S3 backend block.
  4. Granting lock permissions to the bucket but not the lock table. The run fails with a confusing AccessDenied. Give the role dynamodb:GetItem, PutItem and DeleteItem on the table.
  5. Killing CI jobs with SIGKILL. Configure runners to send SIGINT and allow a grace period so Terraform can release the lock.
  6. Assuming locking prevents drift. A lock serialises Terraform runs; it does nothing about someone editing resources in the console. Run regular plans for that.

Frequently asked questions

Does terraform plan take a lock?

Yes, because refresh may update state. Use -lock=false only for read-only speculative plans in pull-request checks where no state write can happen, and never for apply.

Does locking slow Terraform down?

Negligibly. Acquiring and releasing a lock is two small API calls. The waiting you experience is other runs genuinely holding the lock.

Can I lock individual resources instead of the whole state?

No. Locks are per state file. If contention is a problem, split large root modules into smaller ones with their own state, which also speeds up plans.

Should I still create a DynamoDB table for new projects?

Not if everyone on the team runs Terraform 1.10 or later. Use use_lockfile = true and skip the table. Keep DynamoDB only while older Terraform versions are still in use.

Key takeaways

  • State locking prevents two Terraform runs from writing state at once, which would otherwise lose changes and orphan resources.
  • Azure, GCS, HCP Terraform, Consul and PostgreSQL backends lock automatically; S3 needs use_lockfile = true or a DynamoDB table.
  • Read the lock metadata before acting; use force-unlock <ID> only when the holding run is confirmed dead, and never disable locking to work around it.
  • Use -lock-timeout for queued pipelines and let CI runners shut Terraform down gracefully.

Want to see locking, remote state and CI pipelines working together on real AWS and Azure accounts? Our DevOps course covers production Terraform workflows with mentor support and placement assistance. For bite-sized video explanations, subscribe to our YouTube channel.

AG
Written byAnurag Gupta

Part of the Techknowledgehub team of industry mentors, writing practical guides to help you build a job-ready tech career.

More articles by Anurag Gupta β†’
Keep reading

Related articles

Leave a Reply