hashicorp/terraform · error

failed to lock state in Postgres

Error message

failed to lock state in Postgres: %s

What it means

Thrown during Backend.StateMgr init when a workspace does not yet exist and stateMgr.Lock(lockInfo) returns an error while trying to take the initialization lock. The lock is needed to safely write an empty sentinel state so Workspaces() will list it. The %s wraps the underlying lock error (often a *statemgr.LockError from the pg client's advisory-lock logic).

Solutions

  1. Inspect the wrapped error - if it is 'Workspace is already locked' or 'Already locked for workspace creation', wait for the other process or run `tofu force-unlock`.
  2. Verify the database connection (host, port, sslmode) and credentials in the backend config.
  3. Re-run `tofu init` after the competing process completes.
  4. Check the Postgres logs for advisory_lock contention or connection kills.
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: ensure no advisory lock is already held for this workspace
var holder string
err := db.QueryRow(`SELECT mode FROM pg_locks WHERE locktype='advisory' AND objid=$1`, wsID).Scan(&holder)
if err == nil { return fmt.Errorf("workspace %s already locked", name) }

Type guard

func isAlreadyLockedErr(err error) bool {
    return err != nil && (strings.Contains(err.Error(), "already locked") || strings.Contains(err.Error(), "Workspace is already locked"))
}

Try / catch

lockId, err := stateMgr.Lock(lockInfo)
if err != nil {
    var le *statemgr.LockError
    if errors.As(err, &le) { return fmt.Errorf("workspace busy, holder: %s; run force-unlock %s", le.Info.Who, le.Info.ID) }
    return err
}

Prevention

When it happens

Trigger: stateMgr.Lock(lockInfo) fails in the !exists branch of StateMgr. Causes: the pg advisory lock is already held (see errors 368-370), the DB connection was lost, or the state table/row is in an inconsistent state.

Common situations: Two concurrent `tofu init` runs for a new workspace; a previous init crashed holding the advisory lock; DB failover between Lock and the subsequent WriteState; insufficient DB privileges on the state table.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/ac1f5089f75817ab. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/pg/backend_state.go:97

	}

	exists := false
	for _, s := range existing {
		if s == name {
			exists = true
			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)

View on GitHub (pinned to d32a084675)