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

  1. 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).
  2. Check Postgres for idle-in-transaction sessions holding the lock: `SELECT * FROM pg_locks WHERE locktype='advisory';`.
  3. Re-run `tofu init` then `tofu apply` once the lock is cleared.
  4. 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

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


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)