hashicorp/terraform · warning

error unlocking Postgres state: %s

Error message

error unlocking Postgres state: %s

What it means

Thrown by the lockUnlock cleanup helper in pg/backend_state.go:103 when stateMgr.Unlock(lockId) fails during the first-time-init path. It surfaces after the primary operation already hit an error (or completed) and the compensating release of the Postgres advisory lock itself failed. Because advisory locks are session-bound, this commonly means the DB session was lost between acquiring and releasing.

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 c9def3e214)

Solutions

  1. Query pg_locks for the workspace lock id (or -1 creation lock) to confirm no stale advisory lock remains.
  2. If a stale lock exists, pg_terminate_backend() the holding pid.
  3. Retry `terraform init`; the failed partial state is cleaned up by re-running.
  4. Check DB/pooler connectivity and idle timeout settings.
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm the advisory lock the init path will use is free before starting
// SELECT count(*) FROM pg_locks WHERE locktype='advisory' AND objid IN (-1, <wsid>);

Try / catch

// Treat unlock-cleanup failures as warnings; the primary op may still be salvageable
// if unlockErr := stateMgr.Unlock(lockID); unlockErr != nil {
//   log.Warn("unlock failed; may need manual advisory lock release", "err", unlockErr)
// }

Prevention

When it happens

Trigger: The Postgres connection dropped between Lock and Unlock; pg_advisory_unlock query errored; a pooler (e.g. PgBouncer in transaction mode) recycled the session so the lock is no longer held by this connection; the lock was already released out-of-band.

Common situations: Network blip to Postgres mid-init; PgBouncer resetting the backend; long init interrupted by a timeout; another process force-unlocked the advisory lock concurrently.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/1c4953d4456dde37. Report an issue: GitHub.