11.2 State Locking
Key Takeaways
- If the backend supports locking, Terraform locks state automatically on every operation that could write state; a failed lock stops the command
- You normally see no lock message; a status line appears only when acquiring the lock takes longer than expected
- Not every backend locks, and they do not lock equally: local uses system APIs, S3 locking is opt-in, http locking needs lock_address
- On Terraform 1.12 the S3 backend enables native locking with use_lockfile = true (default false); dynamodb_table is deprecated
- terraform force-unlock LOCK_ID is a last resort for a lock you own after automatic unlock failed; it does not change infrastructure
11.2 State Locking
Quick Answer: If the backend supports it, Terraform automatically locks state for every operation that could write state. You usually see no message. If the lock cannot be acquired, the command stops. Not every backend locks, and S3 locking is opt-in.
terraform force-unlock LOCK_IDis a last resort for a lock you own after automatic unlock failed.
Objective 6b on Terraform Associate (004) is about preventing concurrent writers. Official references: State Locking, terraform force-unlock, and the Terraform 1.12 S3 backend page.
Why a lock exists
State is the binding database. Two applies that read the same snapshot, each create an object, and each write a new snapshot will lose one writer's bindings. The leftover remote object is unmanaged. The overwritten snapshot may point at objects the other writer already replaced. Locking serializes writers so only one process updates that snapshot at a time.
HashiCorp's locking page states the rule in one paragraph:
- If the backend supports locking, Terraform locks state for all operations that could write state.
- Locking happens automatically. You do not see a message that it happened.
- If state locking fails, Terraform does not continue.
- You can disable locking on most commands with
-lock=false. HashiCorp does not recommend it. - If acquiring the lock takes longer than expected, Terraform prints a status message. Silence does not mean "no lock" — it means the lock was acquired quickly, or the backend does not lock.
Read-only commands that never write state do not need the same exclusive lock. terraform state list and terraform state show are inspection. terraform apply, terraform destroy, terraform refresh / refresh-only apply, and mutating terraform state subcommands (mv, rm, replace-provider) are writers.
# Dangerous: skip the lock. Do not make this a habit.
terraform apply -lock=false
# Wait up to two minutes if another run still holds the lock.
terraform apply -lock-timeout=2m
-lock-timeout retries acquisition. The default on terraform init is 0s: fail immediately if the lock is already held. A timeout is how a pipeline waits for a teammate's apply to finish. It is not a reason to use -lock=false.
Backends do not lock equally
HashiCorp: not all backends support locking. You must read the page for that backend. On Terraform 1.12 the built-in types are local, remote, s3, azurerm, gcs, http, consul, kubernetes, oss, cos, oci, and pg. You cannot load extra backends as plugins.
| Backend (1.12 docs) | Locking quality you should remember |
|---|---|
local | Locks with system APIs on that machine. Two processes in one directory are serialized. Two laptops are not. Another process cannot force-unlock a local file. |
s3 | Locking is opt-in. Native S3 locking is use_lockfile = true (default false). dynamodb_table still works and is deprecated. |
azurerm | Supports locking and consistency checking with Azure Blob Storage native capabilities. |
http | Locking is optional. You must set lock_address / unlock_address. Without those, HTTP storage does not lock. |
consul, gcs, kubernetes, remote | Documented as backends that can lock; still verify the type's page for the flags that type requires. |
The exam trap is treating "we use S3, therefore we lock" as a fact. On Terraform 1.12, a bare backend "s3" { bucket, key, region } stores state and does not lock until you turn locking on.
S3 locking on Terraform 1.12 (verify this, do not recycle old blogs)
The official v1.12.x S3 page says:
- State locking is an opt-in feature of the S3 backend.
- Locking can be enabled via S3 or DynamoDB.
- DynamoDB-based locking is deprecated and will be removed in a future minor version.
- To migrate from older Terraform that only knew DynamoDB locking, you may configure S3 and DynamoDB arguments at the same time.
- Native S3 locking:
use_lockfile(optional, defaults to false) writes a lock object next to the state key, named<key>.tflock. - Deprecated DynamoDB locking:
dynamodb_tablenaming a table whose partition key isLockID(String).
terraform {
backend "s3" {
bucket = "tfstate-prod-004"
key = "network/terraform.tfstate"
region = "us-east-1"
use_lockfile = true
}
}
Historically, teams added a DynamoDB table because S3 alone did not lock. That history is why so many older study notes say "S3 plus DynamoDB." For 004 / Terraform 1.12, quote the current page: prefer use_lockfile = true; treat dynamodb_table as the deprecated migration path; do not claim S3 locks by default.
IAM for native locking also needs s3:GetObject, s3:PutObject, and s3:DeleteObject on the .tflock object. Those permissions are not required on the state object for delete — Terraform does not delete the state file. Enable bucket versioning so a bad write can be recovered. Versioning is not locking; it is recovery.
sequenceDiagram
participant A as Engineer A apply
participant L as Backend lock
participant S as State snapshot
participant B as Engineer B apply
A->>L: Acquire lock
L-->>A: Lock held
A->>S: Refresh, plan, write new snapshot
B->>L: Acquire lock
L-->>B: Failed or waiting
Note over B: Command stops unless lock-timeout eventually succeeds
A->>L: Release lock
B->>L: Acquire lock
B->>S: Now the only writer
force-unlock is a last resort
Usage: terraform force-unlock [options] LOCK_ID.
Official behavior:
- It manually unlocks state when automatic unlocking failed.
- It does not modify infrastructure.
- The lock's behavior depends on the backend.
- Local state files cannot be unlocked by another process.
- Be very careful. Unlocking a lock someone else still holds creates multiple writers.
- Use it only to unlock your own lock after automatic unlock failed (crash, killed CI job, lost session).
- The command requires the unique lock ID Terraform printed when unlock failed. That ID is a nonce so you target the correct lock.
-forceskips the confirmation prompt.
# Terraform printed this ID after a crashed apply left the lock behind.
terraform force-unlock 9a3c1e2b-77d4-4c10-9f08-0b6a0d2e4a11
Do not script force-unlock at the start of every pipeline. Do not invent a lock ID. If another apply is still running, wait or use -lock-timeout on your command.
Scenario: the Friday lock
Ava starts terraform apply against an S3 backend with use_lockfile = true. The runner is SIGKILLed mid-apply. The .tflock object remains. Ben's apply fails immediately with a lock error and a lock ID. Ben must not force-unlock while Ava might still be running. After they confirm the runner is dead, Ava (or an owner of that run) force-unlocks that ID. The next apply is a new writer. If the team had omitted use_lockfile and omitted dynamodb_table, both applies would have written the same key with no lock at all.
004 traps for objective 6b
- Locking is automatic for write operations, not a resource you declare in HCL.
- A failed lock aborts; Terraform does not "apply anyway and warn."
-lock=falseexists and is discouraged.- S3 on Terraform 1.12 does not lock until you set
use_lockfile(or the deprecateddynamodb_table). force-unlockneeds the printedLOCK_ID, does not change objects, and is not for someone else's live run.- Local locks do not span two checkouts.
On Terraform 1.12, when does state locking happen if the backend supports it?
What does the official Terraform 1.12 S3 backend documentation say about state locking?
A CI job is killed during apply and leaves a remote lock. What is the correct last-resort recovery?