There is no automatic expiry to rely on: with the S3 backend using DynamoDB locking, a lock item stays in the table until something removes it. Verification is therefore manual and evidence-based — confirm the run that created the lock is genuinely finished or dead, release that one lock by its ID, then re-check state. -lock-timeout does not expire locks; it only controls how long Terraform waits to acquire one.
Confirming the owner is defunct
Work from the lock's own metadata rather than from a feeling that it has been sitting there a while.
- Capture the exact ID. The failed run prints a lock ID in its error output. Keep that value;
force-unlock targets it specifically. - Read the lock item. In DynamoDB the item key (
LockID) is the state path, and the Info attribute holds JSON with ID, Who, Operation, Created, and Version. The ID inside should match the ID from the error.aws dynamodb get-item \
--table-name my-tf-locks \
--key '{"LockID":{"S":"my-bucket/env/prod/terraform.tfstate"}}' \
--consistent-read
- Trace
Who. If it names a CI runner, open that job and confirm it is finished, cancelled, or that the runner no longer exists — not merely quiet. If it names a host you control, look for live processes: ps -eo pid,lstart,cmd | grep -i '[t]erraform'. A wrapper script or a paused shell can keep a run open. - Cross-check the state object.
aws s3api head-object --bucket my-bucket --key env/prod/terraform.tfstate shows LastModified. A state write newer than the lock's Created time while the lock is still present usually means the writer finished but its unlock failed. Treat that as a strong hint, not proof.
Backend timeout options
For S3 with DynamoDB, there is no Terraform-supported lock TTL. DynamoDB's own TTL feature is not wired into Terraform's locking and is a poor substitute: deletion is asynchronous and can lag for hours, so it may remove a lock while a run is still live. Terraform Cloud and Terraform Enterprise behave differently — runs have their own timeouts and locks can be released through the UI or API. Some session-based backends (Consul, for example) release locks when a session expires; confirm that behavior against your backend's documentation before depending on it.
| Mechanism | Effect on a held lock |
|---|
-lock-timeout | Waits longer to acquire; never releases |
| DynamoDB TTL | Not integrated; unsafe as an expiry |
| Terraform Cloud run timeout | Applies to that platform's runs only |
Releasing and verifying
terraform force-unlock <LOCK_ID>\
terraform plan
Prefer force-unlock over hand-deleting the DynamoDB item, because it goes through the backend's own unlock path. Then run terraform plan: it proves you can acquire the lock again and that state reads cleanly. Compare the plan against what you expect. An unexpected diff is the signal to inspect terraform state pull and the state serial before applying anything.
The detail that changes this
If the lock's Who field points to a CI job that is still running, stop there — cancel or wait for that job rather than unlocking. If it points to a runner that no longer exists, the steps above apply. Version behavior of backend locking has been stable across recent Terraform 1.x releases, but confirm against the documentation for your exact backend and version.