hashicorp/terraform · error · statemgr.LockError

Cannot lock workspace; already locked for workspace creation

Error message

Cannot lock workspace; already locked for workspace creation: %s

What it means

Returned by RemoteClient.Lock (pg/client.go:124). The workspace row exists and the per-workspace lock was acquired, but the global creation lock pg_try_advisory_lock(-1) returned false — another session is mid-creation of some workspace. Per the code, Terraform releases the just-acquired workspace lock and fails, because mutating a workspace while another creation is in flight is considered unsafe.

Source

Thrown at internal/backend/remote-state/pg/client.go:124

		var innerDidLock []byte
		err := innerRow.Scan(&innerDidLock)
		if err != nil {
			return "", &statemgr.LockError{Info: info, Err: err}
		}
		if string(innerDidLock) == "false" {
			return "", &statemgr.LockError{Info: info, Err: fmt.Errorf("Already locked for workspace creation: %s", c.Name)}
		}
		info.Path = "-1"
	case err != nil:
		return "", &statemgr.LockError{Info: info, Err: err}
	case string(didLock) == "false":
		// Existing workspace is already locked. Release the attempted creation lock.
		lockUnlock("-1")
		return "", &statemgr.LockError{Info: info, Err: fmt.Errorf("Workspace is already locked: %s", c.Name)}
	case string(didLockForCreate) == "false":
		// Someone has the creation lock already. Release the existing workspace because it might not be safe to touch.
		lockUnlock(string(pgLockId))
		return "", &statemgr.LockError{Info: info, Err: fmt.Errorf("Cannot lock workspace; already locked for workspace creation: %s", c.Name)}
	default:
		// Existing workspace is now locked. Release the attempted creation lock.
		lockUnlock("-1")
		info.Path = string(pgLockId)
	}
	c.info = info

	return info.ID, nil
}

func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {
	return c.info, nil
}

func (c *RemoteClient) Unlock(id string) error {
	if c.info != nil && c.info.Path != "" {
		query := `SELECT pg_advisory_unlock(%s)`
		row := c.Client.QueryRow(fmt.Sprintf(query, c.info.Path))

View on GitHub (pinned to c9def3e214)

Solutions

  1. Retry once the in-flight workspace creation completes — the -1 lock is transient.
  2. Check pg_locks for advisory lock -1 and terminate the holder if stale.
  3. Avoid overlapping workspace creation with active applies on the same backend.
Defensive patterns

Strategy: try-catch

Validate before calling

// Check the creation lock is free before operating on an existing workspace
// SELECT 1 FROM pg_locks WHERE locktype='advisory' AND objid=-1;

Type guard

// Returns *statemgr.LockError, carrying Info about the creation-lock holder
// var lockErr *statemgr.LockError
// if errors.As(err, &lockErr) { /* inspect lockErr.Info */ }

Try / catch

// Retryable: the creation lock is transient
// for i := 0; i < 5; i++ {
//   _, err := client.Lock(info)
//   var le *statemgr.LockError
//   if errors.As(err, &le) && strings.Contains(le.Error(), "workspace creation") {
//     time.Sleep(time.Duration(1<<i) * time.Second); continue
//   }
//   return err
// }

Prevention

When it happens

Trigger: Concurrent first-time creation of a different workspace while you try to operate on an existing one; a stuck session holding the -1 advisory lock.

Common situations: Shared Postgres backend used by multiple teams where one team's workspace creation overlaps another team's apply; stale -1 lock from a crashed create.

Related errors


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