hashicorp/terraform · error
failed to lock state in Consul
Error message
failed to lock state in Consul: %s
What it means
Thrown by the Consul backend's StateMgr during initialization of a non-default workspace. The backend acquires a Consul lock so it can atomically write an empty sentinel state (so States() can later discover the workspace). If stateMgr.Lock returns an error, it is wrapped here. The underlying cause is typically that another client holds the lock, the Consul agent is unreachable, or the ACL token cannot create a session or write the lock key.
Solutions
- Read the wrapped error: if it names a lock holder / lock ID, run `terraform force-unlock <LOCK_ID>` (the ID is the Consul session ID).
- Check Consul health and connectivity from the Terraform host: `consul members`, `consul kv get -recurse <state-path>`.
- Verify the backend's ACL token grants session:write and kv:write on the state prefix; recreate the token if revoked.
- Coordinate with the team so only one run targets the workspace at a time; re-run terraform init/apply once the lock is cleared.
Defensive patterns
Strategy: retry
Validate before calling
// Pre-check whether a Consul lock is already held for the state path
// before attempting StateMgr init.
func consulLockHeld(client *consulapi.Client, statePath string) (bool, error) {
pair, _, err := client.KV().Get(strings.TrimRight(statePath, "/")+".lock", nil)
if err != nil {
return false, err
}
return pair != nil, nil
}
// if held, surface a clear message instead of letting Lock() fail opaquely. Try / catch
// Treat statemgr.LockError specially: surface the lock ID so the user can force-unlock.
sm, diags := backend.StateMgr(name)
if diags.HasErrors() {
var le *statemgr.LockError
for _, d := range diags {
if errors.As(d.Err(), &le) && le.Info != nil {
return fmt.Errorf("state %q is locked (id=%s); run terraform force-unlock %s", name, le.Info.ID, le.Info.ID)
}
}
return diags.Err()
} Prevention
- Run only one Terraform operation per workspace at a time; use CI concurrency limits.
- Ensure the Consul ACL token has session:write and kv:write on the state prefix.
- Use the lock = true default; do not disable locking to dodge contention.
- Wire terraform force-unlock into your runbook so crashed runs are cleaned up promptly.
When it happens
Trigger: backend.StateMgr(name) with name != backend.DefaultStateName -> stateMgr.Lock(lockInfo) returns a non-nil error. The lock fails inside RemoteClient.lock() when createSession() or consulLock.Lock(...) errors out (e.g. another session holds the key, LockWaitTime elapsed after LockTryOnce, ACL denied, network error).
Common situations: A previous Terraform run crashed without releasing its Consul session; a teammate is running terraform apply against the same workspace; the Consul ACL token used by the backend lacks session:write or kv:write on the state prefix; the Consul agent is down or partitioned; the session TTL expired mid-run.
Related errors
- Error unlocking Consul state. Lock ID
- error unmarshaling lock info
- state already locked
- Can't serialize backend configuration as JSON
- confirmFunc must not be nil
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/e2303acc2aef9765.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/consul/backend_state.go:108
}
if !b.lock {
stateMgr.DisableLocks()
}
// 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 stateView on GitHub (pinned to d32a084675)