hashicorp/terraform · error
Error unlocking Consul state. Lock ID
Error message
Error unlocking Consul state. Lock ID: %s Error: %s You may have to force-unlock this state in order to use it again. The Consul backend acquires a lock during initialization to ensure the minimum required key/values are prepared.
What it means
After acquiring the init lock for a non-default workspace, the backend runs RefreshState/WriteState/PersistState inside a lockUnlock helper. If any of those steps fails AND the subsequent stateMgr.Unlock(lockId) also fails, this formatted errStateUnlock is raised and the user is told to force-unlock. It indicates Terraform could not cleanly release the lock it just took.
Solutions
- Capture the Lock ID printed in the message and run `terraform force-unlock <LOCK_ID>` (Consul session ID).
- Inspect `consul session list` to see whether the session still exists; if gone, the lock will auto-release but force-unlock clears the local view faster.
- Verify Consul connectivity (`consul info`, `consul members`) and ACL token validity before retrying.
- Re-run the Terraform command once the lock is released.
Defensive patterns
Strategy: try-catch
Try / catch
// After a failed init that may have left a lock, attempt force-unlock once.
sm, diags := backend.StateMgr(name)
if diags.HasErrors() {
msg := diags.Err().Error()
if strings.Contains(msg, "force-unlock") {
if id := extractLockID(msg); id != "" {
log.Printf("init failed with dangling lock %s; attempting force-unlock", id)
if _, uerr := sm.Unlock(id); uerr != nil {
return fmt.Errorf("manual recovery required: %w", uerr)
}
}
}
return diags.Err()
} Prevention
- Keep Consul well-connected from the Terraform host; prefer a local agent.
- Tune session TTLs only with care; shorter TTLs make this more likely.
- Do not revoke the backend ACL token mid-run.
- Have a force-unlock runbook ready; capture lock IDs from CI logs.
When it happens
Trigger: In backend_state.go StateMgr: RefreshState/WriteState/PersistState returns an error; lockUnlock(parent) calls stateMgr.Unlock(lockId) which returns non-nil; the helper returns fmt.Errorf(errStateUnlock, lockId, err).
Common situations: Consul session TTL (15s default) expired while Terraform was still processing; Consul was restarted or lost quorum mid-operation; network dropped between the Terraform host and Consul; the ACL token was revoked during the run.
Related errors
- error unmarshaling lock info
- failed to lock state in Consul
- Lock ID should be numerical value, got
- state already locked
- Unlocking the state file on TencentCloud cos backend…
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/819034e32b9fd216.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/consul/backend_state.go:114
// the default state always exists
if name == backend.DefaultStateName {
return stateMgr, nil
}
// Grab a lock, we use this to write an empty state if one doesn't
// exist already. We have to write an empty state as a sentinel value
// so States() knows it exists.
lockInfo := statemgr.NewLockInfo()
lockInfo.Operation = "init"
lockId, err := stateMgr.Lock(lockInfo)
if err != nil {
return nil, diags.Append(fmt.Errorf("failed to lock state in Consul: %s", err))
}
// Local helper function so we can call it multiple places
lockUnlock := func(parent error) error {
if err := stateMgr.Unlock(lockId); err != nil {
return fmt.Errorf(strings.TrimSpace(errStateUnlock), lockId, err)
}
return parent
}
// Grab the value
if err := stateMgr.RefreshState(); err != nil {
err = lockUnlock(err)
return nil, diags.Append(err)
}
// If we have no state, we have to create an empty state
if v := stateMgr.State(); v == nil {
if err := stateMgr.WriteState(states.NewState()); err != nil {
err = lockUnlock(err)
return nil, diags.Append(err)
}
if err := stateMgr.PersistState(nil); err != nil {View on GitHub (pinned to d32a084675)