Why a Dead Process Leaves a Live Lock
Every operation that would write state takes an exclusive lock first, and most that would merely read consistent state take a shared one. The mechanism differs by backend — the local backend writes a .terraform.tfstate.lock.info file beside the state, S3 records a conditional-write item in DynamoDB (the lock table from this phase), Terraform Cloud registers the lock against the workspace's run — but the contract is uniform: only one writer at a time. Two concurrent applies without that guarantee would interleave state writes, and whichever finished last would erase the other's record of created objects, orphaning real infrastructure that nothing tracks. The lock is the database's row lock, applied to your entire estate.
The lock's weakness is its owner's lifecycle. A lock is released by the process that took it — on successful completion, or on a graceful interrupt. When the process dies without that release path — a CI runner reclaimed mid-apply, kill -9, a laptop that sleeps through a VPN drop, an IDE crash — the backend never hears the goodbye, and the lock outlives the operation. Note the granularity: the lock covers the entire state object, not individual resources, so one stuck apply blocks every other change to that stack — a deliberate design choice, because partial state writes are precisely what interleaving would corrupt. Every subsequent command that needs the lock fails, identically, for everyone:
Error: Error acquiring the state lock
Error message: ConditionalCheckFailedException: The conditional request failed
Lock Info:
ID: 2e9dfc30-4d33-4ba5-b3ae-20e19c37a1c2
Path: acme-tfstate-prod/prod/network/terraform.tfstate
Operation: Apply
Who: alice@laptop
Version: 1.6.2
Created: 2026-01-10T09:14:22Z
Note what the error is not: it is not corruption, and it is not data loss. A stale lock is a stopped door, not a broken building — Terraform is refusing to proceed precisely because it cannot guarantee the previous writer finished cleanly. The Lock Info block is the backend telling you everything it knows about the holder: the ID you will need to force-unlock, the Operation that took it (Apply, Plan, Unlock), Who (CI service account or a human's workstation), and Created — the age, which is the single most decision-relevant field. A lock created forty seconds ago belongs to a teammate's run in flight; one created four hours ago on a laptop that has since been closed is the stale kind.
The trap for the uninitiated is treating the error as an obstacle to route around. -lock=false on a plan, hand-editing state, pointing at a different workspace — each of these converts a one-minute recovery into a genuine incident. The correct response is the next chapter's subject: diagnose what holds the lock before deciding whether it should be broken. One prevention note worth internalizing before the incident happens at all: locks and plans interact the same way they do for applies, so two engineers running plan concurrently on a remote backend can contend too — usually resolved by one waiting, occasionally by the same stale-lock error if one of the two plans dies. The habit of checking the CI dashboard before assuming malice extends to plans as well.
Diagnosis First: Who Actually Holds This Lock?
Before any unlock, one question must have an evidence-based answer: is an operation genuinely in progress? Breaking the lock of a live apply is the one move in this entire phase that can corrupt state, so the diagnosis is not bureaucracy — it is the safety interlock. Work the fields in order:
- Created vs now. An age longer than any plausible plan/apply duration for this stack (four hours for an estate that normally applies in six minutes) means stale. An age of ninety seconds means wait.
- Who. A CI service account points at a runner — check the pipeline's run history for an in-flight or killed job. A human's workstation means a message in the team channel: "your apply from 09:14 still holds the state lock — did it finish?" Thirty seconds of asking beats an hour of archaeology.
- Operation. Locks taken by
UnlockorPlanare lower-stakes thanApply— an interrupted plan changes nothing, an interrupted apply may have left state mid-write.
Backends with real locking make this cheap precisely because they record the holder: S3+DynamoDB stores the Lock Info as the item's attributes, readable directly from the table; Terraform Cloud/Enterprise surfaces the holding run in the workspace UI, with a cancel button that releases the lock as a side effect. Teams on such backends recover in minutes; teams that configured state without locking never face stale locks but pay for it in race conditions instead — a trade already covered in Limitations of Local State Files. The habit worth building is to treat the lock error as a notification rather than a wall: paste the Lock Info block into the team channel, and let the person named in the Who field confirm or deny in their own words — the fastest recovery is almost always the one where the holder unlocks it themselves, because they can also tell you whether the state write completed.
While diagnosing, gather read-only evidence that does not need the lock:
terraform state list # state commands take no write lock
terraform show 'aws_instance.web' # attributes intact?
terraform plan -lock=false # what does the estate look like now?
That last command deserves care: -lock=false on a plan is the sanctioned read escape hatch (it may read slightly stale state, which is acceptable for diagnosis), but never pass -lock=false to an operation that writes state or to apply — you would be doing the exact thing the lock exists to prevent, deliberately.
The verdict shapes the next step. Genuinely running → wait, or cancel the run properly in the CI system so it unlocks itself. Confirmed dead → terraform force-unlock with the recorded ID. Unknown → keep asking; "I could not determine the holder" is never a state in which to break a lock, because the cost asymmetry is brutal: waiting costs minutes, corrupting state costs an incident review. When the holder genuinely cannot be identified — an expired CI service account, a departed contractor's laptop — the safe ordering is to confirm with whoever owns the pipeline today, then unlock: the conversation takes five minutes and is the only verification available.
terraform force-unlock: The Procedure and the Risk It Carries
The command is short and deliberately gated:
terraform force-unlock 2e9dfc30-4d33-4ba5-b3ae-20e19c37a1c2
Terraform will remove the lock for the state ID shown below.
Do you really want to force-unlock?
Type 'yes' to confirm:
> yes
Successfully unlocked state 2e9dfc30-4d33-4ba5-b3ae-20e19c37a1c2
Three properties to internalize. First, it requires the lock ID shown when the lock was created — not the path, not the workspace name. There is no wildcard, no "unlock whatever is there": you must name the exact lock, which means you must have read the Lock Info block, which means you diagnosed first. That friction is by design. Second, the typed yes confirmation exists because the command cannot verify what you could not: Terraform does not know whether the holder is alive. Third, force-unlock changes only the lock record — it does not repair whatever the interrupted operation left behind, and after unlocking, state is exactly as the dead process left it. If you pass an ID that does not match the current lock, the command fails rather than unlocking whatever is held; there is no "close enough".
Which is why the risk statement deserves precision. If the interrupted apply wrote state partway — created three of five resources, then died — the lock was protecting that inconsistent window. Breaking it is correct because the writer is gone; running a new apply immediately after is not, until you have verified what the dead run left. Inspect with the lock-free read commands, then plan: resources created-but-unrecorded appear as to be created (and applying blindly risks a name-conflict failure — or worse, a duplicate where the API allows one), while resources recorded-but-half-configured appear as in-place updates. The plan after a forced unlock is your reconciliation report; read it before answering with apply. And if the reconciliation shows something unexpected — a database instance half-created, a security group with rules the dead apply never recorded — prefer repair-by-hand over speed: the lock was broken, not the state, and a careful hour here is cheaper than recreating production from a snapshot later.
Two backend notes round out the operation. With the local backend, the lock is a plain .terraform.tfstate.lock.info file — recovery is deleting that file, and the same rule applies: only when no run is live. With S3+DynamoDB, force-unlock performs a conditional delete of the LockID item, which is why the ID argument is mandatory. And prevention is cheaper than recovery every time: pass -lock-timeout=1m in CI so short lock contention waits instead of failing the job (contention is not staleness), set CI job timeouts shorter than your team's patience so killed runners are visible, and never Ctrl-C an apply you can let finish — a completed apply releases its own lock. The state-operations phase closes here: you can inspect state, relocate entries, adopt objects, declare refactors in code, and recover the lock that stops everything — the foundation on which Module Block Anatomy builds next. Rehearse the unlock once in a development stack; the first time you run it should not be during an incident.
Quiz
1. A CI apply was killed when its runner was reclaimed. Every later plan fails with 'Error acquiring the state lock'. What is the correct first move?
2. What does terraform force-unlock require, and what does it actually do?
3. A teammate's apply genuinely holds the lock and is making progress. What happens if you force-unlock anyway?
4. How do the Lock Info fields drive the recovery decision?
Flashcards
Question
Why does state locking exist, and what does it prevent?
Click to reveal answer
Answer
Only one writer may operate on state at a time (local: .terraform.tfstate.lock.info file; S3: conditional-write item in DynamoDB; TFC: run-associated lock). It prevents two concurrent applies from interleaving state writes — which would erase the record of created objects and orphan real infrastructure.
Question
What leaves a stale lock, and what does the error actually mean?
Click to reveal answer
Answer
A process that dies without its release path: killed CI runner, kill -9, laptop sleep mid-apply, crashed IDE. 'Error acquiring the state lock' + Lock Info means the backend cannot prove the previous writer finished — a stopped door, not a broken state file. The correct reaction is diagnosis, not evasion.
Question
How do you diagnose whether a lock is stale or live?
Click to reveal answer
Answer
Read Lock Info: Created-vs-now (age beyond any plausible run duration = stale), Who (CI account → check pipeline history; human → message them), Operation (Apply risks partial state, Plan does not). Gather lock-free evidence with `terraform state list` / `state show`, and `plan -lock=false` for read-only diagnosis.
Question
What does terraform force-unlock do and not do?
Click to reveal answer
Answer
Removes the lock record for the exact lock ID you supply, after a typed 'yes' confirmation. It does not verify the holder is dead, does not repair or roll back state, and does not quarantine the interrupted apply's effects — hence the mandatory plan-and-reconcile afterward before any new apply.
Question
Name two stale-lock prevention practices and one read-only escape hatch.
Click to reveal answer
Answer
Prevention: `-lock-timeout=1m` in CI so short contention waits instead of failing; CI job timeouts that surface killed runners; let applies finish rather than Ctrl-C. Escape hatch: `-lock=false` on plan (and lock-free `state list`/`show`) for diagnosis — never on apply or any state writer.
Revision Notes
Key Takeaways
- 1. Locking serializes state writers; breaking it under a live apply risks interleaved writes that orphan real infrastructure — the one operation in this phase that corrupts rather than misconfigures
- 2. A stale lock is a dead process's goodby that never arrived — the error blocks operations but has damaged nothing; patience beats evasion
- 3. Lock Info drives the decision: Created gives staleness, Who names the holder, Operation calibrates risk, and ID is the mandatory force-unlock argument
- 4. force-unlock removes only the lock record; the plan afterward is your reconciliation report on what the interrupted apply left behind
- 5. Prevention is cheaper: -lock-timeout so contention waits, CI timeouts that surface killed runners, and -lock=false only for read-only diagnosis
Interview Tips
- • "A deploy pipeline fails with 'Error acquiring the state lock' — walk me through recovery." — read Lock Info, verify the holder is dead (age, Who, CI history), force-unlock with the ID, then plan to reconcile partial effects before applying; this sequence shows you understand risk, not just commands
- • "When would you NOT force-unlock?" — when the holder might be live (young lock, unanswered teammate, unknown state); the asymmetry — minutes of waiting vs corrupted state — is the whole argument
- • "How does locking work in S3 + DynamoDB?" — a conditional PutItem on the LockID-keyed item is the lock acquisition; the item stores Lock Info as attributes; force-unlock is the conditional delete; mention -lock-timeout as the contention-vs-staleness knob
- • "Difference between a stale lock and corrupt state?" — a stale lock is an unreleased mutex: nothing is damaged, operations are merely refused. Corruption comes from writing without the lock (or unlocking under a live writer). Diagnosis separates the two before any action
Cheat Sheet
State Lock Breakage and Recovery
The error:
Error acquiring the state lock
Lock Info:
ID: 2e9dfc30-... # the unlock argument
Operation: Apply # Apply = possible partial write
Who: alice@laptop # verify: CI history or a message
Created: 09:14:22Z # age ⇒ stale or in-flight?
Recovery decision:
- Live run (young lock / CI job active)? → wait or cancel the run properly (it unlocks itself).
- Confirmed dead? →
terraform force-unlock <LOCK_ID>→ typeyes. - After unlocking:
terraform state list+terraform plan— reconcile what the dead run left BEFORE applying.
Read-only diagnosis (no lock needed):
terraform state list
terraform show 'aws_instance.web'
terraform plan -lock=false # plan only — NEVER -lock=false on apply
Prevent: -lock-timeout=1m (contend-wait, not fail) · CI job timeouts · don't Ctrl-C applies you can finish.
Backend note: local backend lock = .terraform.tfstate.lock.info file — delete it manually, same rule (only when nothing is live).