hashicorp/terraform · error
error unlocking Postgres state
Error message
error unlocking Postgres state: %s
What it means
Wrapped by the lockUnlock helper inside Backend.StateMgr when stateMgr.Unlock(lockId) fails after the init lock was acquired but a later step (WriteState/PersistState/Refresh) errored. This is cleanup-path noise: the original error is chained as parent, and the unlock failure is surfaced alongside it. It means the advisory lock may still be held after the run aborted.
Solutions
- Run `tofu force-unlock <lock-id>` to clear the stuck advisory lock (or SELECT pg_advisory_unlock(<id>) from a psql session if the holding session is gone).
- Check Postgres for idle-in-transaction sessions holding the lock: `SELECT * FROM pg_locks WHERE locktype='advisory';`.
- Re-run `tofu init` then `tofu apply` once the lock is cleared.
- Ensure the DB connection has keepalive/idle timeout settings that do not kill sessions mid-apply.
Defensive patterns
Strategy: try-catch
Validate before calling
// ensure the holding session is alive; advisory locks are session-scoped // run `SELECT pg_advisory_unlock(<id>)` from the SAME session that locked
Try / catch
err = lockUnlock(parentErr)
if err != nil {
log.Printf("WARN unlock failed; manual `tofu force-unlock %s` may be required: %v", lockId, err)
return parentErr
} Prevention
- Do not let the DB kill idle sessions mid-apply (raise idle timeout).
- Always report the lock ID in unlock-failure logs so operators can force-unlock.
- Monitor pg_locks for advisory locks from killed sessions.
When it happens
Trigger: stateMgr.Unlock(lockId) returns err != nil inside the lockUnlock closure, which is called from RefreshState/WriteState/PersistState error branches. Causes: DB connection dropped between acquiring the advisory lock and releasing it, the lock row was deleted out-of-band, or pg_advisory_unlock returned false/errored.
Common situations: Postgres connection timeout during apply; DB restarted mid-run; another admin manually released the advisory lock; connection pool evicted the session that held the session-level advisory lock.
Related errors
- Already locked for workspace creation
- Cannot lock workspace; already locked for workspace creation
- Error unlocking S3 state. Lock ID
- failed to lock state in Postgres
- Workspace is already locked
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/1c4953d4456dde37.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/pg/backend_state.go:103
break
}
}
// 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 Workspaces() knows it exists.
if !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 Postgres: %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(`error unlocking Postgres state: %s`, err)
}
return parent
}
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 {
err = lockUnlock(err)
return nil, diags.Append(err)
}
}
// Unlock, the state should now be initialized
if err := lockUnlock(nil); err != nil {
return nil, diags.Append(err)View on GitHub (pinned to d32a084675)