Fix Terraform S3 Backend Error Acquiring State Lock

Tested on Ubuntu 26.04 LTS (Resolute Raccoon)
Package terraform 1.16.1
hashicorp/aws 6.63.0
awscli 2.36.38
Applies to Any host with Terraform 1.10+ and an S3 remote state backend; examples use ap-south-1
Privilege AWS identity permitted to access the configured S3 backend and .tflock object; inspection examples also require s3:ListBucket
Scope Diagnose and fix Error acquiring the state lock when Terraform uses an S3 backend — S3 native locking with use_lockfile and the terraform.tfstate.tflock object, legacy dynamodb_table locking, live lock contention, stale locks after crashed runs, terraform force-unlock, and IAM gaps on the .tflock path. Does not cover full S3 backend bootstrap, Consul or HCP Terraform locking, or credential errors (ExpiredToken, missing credentials).
Related guides Terraform troubleshooting
Short on time?Beta Get the gist of this 9-minute read in seconds, then ask follow-up questions

Another terraform plan or apply stopped before it touched your resources:

text
Error: Error acquiring the state lock

Error message: operation error S3: PutObject, https response error
StatusCode: 412, RequestID: …, api error PreconditionFailed: At least one of the pre-conditions you
specified did not hold
Lock Info:
  ID:        9ca30262-a33c-ae75-eb09-9b76db1f3f00
  Path:      glc-tfstate-lab-342524208658/s3-lock-error-lab/terraform.tfstate
  Operation: OperationTypeApply
  Who:       root@golinuxcloud
  Version:   1.16.1
  Created:   2026-09-05 05:58:31.770607664 +0000 UTC

That pattern means Terraform tried to take an S3 native lock (use_lockfile) and another writer already holds it. It is not the same failure shape as the older DynamoDB lock table, and the fix depends on which mechanism your backend block actually uses.


What the S3 state lock error means

On an S3 backend with use_lockfile = true (Terraform 1.10+), Terraform stores lock metadata in a companion object beside state:

text
s3://<bucket>/<key>                 ← state file
s3://<bucket>/<key>.tflock          ← lock metadata (JSON)

One run conditionally creates the .tflock object containing a unique lock ID. If the object already exists, another run cannot acquire the lock. Concurrent runs fail immediately by default because -lock-timeout defaults to 0s; setting -lock-timeout makes Terraform retry for the specified period. The holder deletes the .tflock object when the command finishes. If the process dies first, the object can remain and block every later run until you clear a stale lock.

For how Terraform state locking works across backends and the general force-unlock workflow, see Terraform state locking. This guide focuses on diagnosing S3 .tflock and legacy DynamoDB backend failures. For bucket creation, versioning, and first-time use_lockfile setup, follow Configure S3 bucket as Terraform backend.


Tell .tflock locking apart from DynamoDB

HashiCorp deprecated DynamoDB-based S3 locking in favor of native S3 lock files. Stacks may still list dynamodb_table during migration; new projects should set use_lockfile = true instead of adding a DynamoDB table only for Terraform.

Read the error message, not only the headline:

Signal in the error Lock mechanism Where Terraform stores the lock
operation error S3: PutObject … PreconditionFailed S3 use_lockfile <state-key>.tflock in the same bucket
ConditionalCheckFailedException on DynamoDB Legacy dynamodb_table Named DynamoDB table (not an S3 object)
AccessDenied on PutObject for a key ending in .tflock S3 use_lockfile (IAM gap) IAM policy missing lock object permissions
No lock error, but two applies corrupt state Neither enabled Backend has no use_lockfile and no dynamodb_table

The Path line in Lock Info points at the state object (bucket/prefix/terraform.tfstate). The companion lock object is always the same key with .tflock appended.

Example backend for S3 native locking:

hcl
terraform {
  backend "s3" {
    bucket       = "glc-tfstate-lab-342524208658"
    key          = "s3-lock-error-lab/terraform.tfstate"
    region       = "ap-south-1"
    use_lockfile = true
  }
}

Legacy DynamoDB locking (deprecated — migrate away):

hcl
terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "ap-south-1"
    dynamodb_table = "terraform-locks"
  }
}
IMPORTANT
Removing dynamodb_table without adding use_lockfile = true leaves the S3 backend without locking. Terraform will not warn you on every plan — you only notice when concurrent writes collide. When you migrate, enable use_lockfile in the same change that retires DynamoDB, then run terraform init with -reconfigure after you confirm no stale DynamoDB lock rows remain.

Read the lock metadata before you act

Every Error acquiring the state lock block includes Lock Info. Copy these fields into your ticket or shell notes:

  • ID — UUID required for terraform force-unlock
  • Operation — what the holder was doing (OperationTypeApply, OperationTypePlan, etc.)
  • Who — hostname and user (or CI agent name) that took the lock
  • Created — UTC timestamp; compare against pipeline history
  • Path — bucket/key for the state object

List objects under your state prefix to see whether a .tflock file is still present:

bash
aws s3api list-objects-v2 --bucket glc-tfstate-lab-342524208658 --prefix s3-lock-error-lab/ --query 'Contents[].Key' --output text

Sample output:

output
s3-lock-error-lab/terraform.tfstate	s3-lock-error-lab/terraform.tfstate.tflock

Both keys should appear when a lock is active. After a successful unlock or plan, only terraform.tfstate remains.

Inspect the lock JSON when you need proof the lock is orphaned:

bash
aws s3 cp s3://glc-tfstate-lab-342524208658/s3-lock-error-lab/terraform.tfstate.tflock -

Sample output (one line):

output
{"ID":"30da14f8-3074-f0d3-6de6-d039025566c9","Operation":"OperationTypeApply","Info":"","Who":"root@golinuxcloud","Version":"1.16.1","Created":"2026-09-05T06:01:31.379408896Z","Path":"glc-tfstate-lab-342524208658/s3-lock-error-lab/terraform.tfstate"}

The ID inside the JSON matches the UUID in Terraform's error text. Use that value for force-unlock, not the S3 object path.


Decide whether the lock is live or stale

text
Is another terraform plan/apply still running against this state?
    ↓ yes
Wait, or retry with -lock-timeout. Do not force-unlock.

    ↓ no — confirmed no holder (CI job ended, process killed)
terraform force-unlock <ID>, or delete only the .tflock object

The state locking lesson covers the full live-versus-stale decision flow and when force-unlock is safe across backends.


Fix a live lock (wait or retry)

When another run legitimately holds the .tflock object, wait for it to finish. To queue behind an in-flight apply in CI, pass a timeout — the default is 0s, so a conflicting run fails on the first attempt:

bash
terraform plan -lock-timeout=5m

Terraform polls until the lock clears or the timeout expires.


Fix a stale S3 .tflock lock

After you confirm no holder is running, clear the lock with Terraform's supported command. Copy the ID from the error or from the .tflock JSON:

bash
terraform force-unlock -force 30da14f8-3074-f0d3-6de6-d039025566c9

Sample output:

output
Terraform state has been successfully unlocked!

The state has been unlocked, and Terraform commands should now be able to
obtain a new lock on the remote state.

Verify the lock object disappeared:

bash
aws s3api list-objects-v2 --bucket glc-tfstate-lab-342524208658 --prefix s3-lock-error-lab/ --query 'Contents[].Key' --output text

Sample output:

output
s3-lock-error-lab/terraform.tfstate

Run a read-only plan to confirm the backend is healthy again:

bash
terraform plan

Sample output (trimmed):

output
No changes. Your infrastructure matches the configuration.

Manual .tflock deletion (emergency only)

If terraform force-unlock cannot remove a confirmed stale lock but you still have direct access to the S3 backend, manual deletion of the .tflock object is the last-resort option:

bash
aws s3 rm s3://glc-tfstate-lab-342524208658/s3-lock-error-lab/terraform.tfstate.tflock

Only do this when:

  • No terraform process is running against that state
  • You delete only the .tflock object, not terraform.tfstate
  • You run terraform plan immediately after deletion

Deleting .tflock does not modify the state file. Never delete terraform.tfstate to fix a lock error — that removes your infrastructure snapshot, not the lock.


Migrate from DynamoDB locking to use_lockfile

Teams upgrading from Terraform versions before native S3 locking often have a dynamodb_table in backend config. A safe migration path:

  1. Ensure no run holds a DynamoDB lock (terraform plan succeeds).
  2. Add use_lockfile = true while keeping dynamodb_table temporarily — current Terraform acquires both locks during cutover, and both must succeed.
  3. Run terraform init -reconfigure from the working directory that owns the backend block.
  4. Confirm new locks appear as .tflock objects beside state.
  5. Remove dynamodb_table from the backend block after every workspace has moved.
  6. Delete the DynamoDB table only when no other state file still references it.

If you skip step 2 and only delete dynamodb_table, Terraform stops using DynamoDB but does not automatically enable S3 locking — you run unprotected until use_lockfile is set.


Fix IAM when .tflock writes are denied

S3 native locking needs permissions on both the state key and the lock key. A policy that allows s3:PutObject on …/terraform.tfstate but not on …/terraform.tfstate.tflock produces AccessDenied during lock acquisition even when state reads work.

Minimum API actions for use_lockfile (per HashiCorp S3 backend docs):

  • s3:ListBucket on the bucket
  • s3:GetObject and s3:PutObject on the state key
  • s3:GetObject, s3:PutObject, and s3:DeleteObject on <key>.tflock

Extend your state bucket IAM policy with the lock suffix, or use a prefix grant that covers both objects under the same key path.


Troubleshooting

Symptom Likely cause Fix
S3: PutObject … PreconditionFailed Another run holds the S3 .tflock lock Wait, use -lock-timeout, or force-unlock after confirming the holder exited
.tflock still listed after CI job ended Stale lock object after crash or kill -9 terraform force-unlock <ID> or delete the .tflock object, then terraform plan
ConditionalCheckFailedException (DynamoDB) Legacy dynamodb_table lock, not .tflock Clear the DynamoDB lock item or force-unlock; migrate to use_lockfile
AccessDenied on PutObject … .tflock IAM policy omits lock object Add GetObject/PutObject/DeleteObject on <key>.tflock
No lock errors but concurrent applies corrupt state Neither use_lockfile nor dynamodb_table configured Add use_lockfile = true on Terraform 1.10+ and re-run terraform init -reconfigure
force-unlock says lock not found but plan still fails Wrong lock ID or different backend key/workspace Match Path in the error to your backend key; list .tflock under that prefix

Summary

Error acquiring the state lock with S3: PutObject and PreconditionFailed points at S3 native locking (use_lockfile) and a .tflock object that already exists — not at a DynamoDB ConditionalCheckFailedException. Read Lock Info to see who holds the lock and when it was taken, list the state prefix for terraform.tfstate.tflock, and match the lock ID in the error to the JSON inside that object.

Live contention on a shared backend clears when the holder finishes or when you retry with -lock-timeout. Stale .tflock objects after a crashed run need terraform force-unlock or, as a last resort, direct deletion of the lock object followed by terraform plan. When migrating off DynamoDB, configure use_lockfile before removing dynamodb_table, and grant s3:ListBucket plus object permissions on both the state key and <key>.tflock. Backend configuration and init flags are covered in Terraform backends and remote state.


References


Frequently Asked Questions

Does terraform force-unlock delete the .tflock object in S3?

Yes. For S3 backends with use_lockfile, terraform force-unlock removes the lock object when the ID matches. After a successful unlock, list the state prefix and confirm only terraform.tfstate remains before you run apply.

Can I use both dynamodb_table and use_lockfile?

HashiCorp allows dynamodb_table and use_lockfile to be configured together during migration. Current Terraform then acquires both locks, which helps maintain coordination while environments are being moved away from DynamoDB. New projects should not add DynamoDB solely for Terraform locks. Plan to drop dynamodb_table once every workspace writes .tflock locks and no stale DynamoDB items remain.

Why does Lock Info Path not mention .tflock?

The Path field names the state object (bucket/key), not the lock file. Terraform always derives the lock object name by appending .tflock to the state key. List the prefix in S3 or open the lock JSON to see the companion object.

Is -lock=false safe in CI?

No. -lock=false skips locking for that command only. Two overlapping jobs can then write the same state snapshot. Use -lock-timeout when jobs queue legitimately, and fix stale locks with force-unlock instead of disabling protection.

Finished reading?Beta Get the key takeaways in seconds, then ask follow-up questions
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with 15+ years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive experience, he excels across development, DevOps, networking, and security, delivering robust and efficient solutions for diverse projects.

  • Go (programming language)
  • Python (programming language)
  • DevOps
  • Computer Security
  • Cloud Computing
  • Kubernetes
  • Linux
  • Ansible (software)