hashicorp/terraform · error

Already locked for workspace creation

Error message

Already locked for workspace creation: %s

What it means

Thrown by RemoteClient.Lock in the pg backend in the sql.ErrNoRows branch (workspace row does not exist, so a new workspace is being created) when the inner pg_try_advisory_lock(-1) returns false. The fixed key -1 is the global 'workspace creation' lock; only one new-workspace creation is allowed at a time across all sessions to serialize the sentinel insert.

Solutions

  1. Wait for the other workspace-creation run to finish, then retry.
  2. If no creation is actually in progress, clear the stuck lock: `SELECT pg_advisory_unlock(-1);` from psql.
  3. Serialize workspace creation in CI (one job at a time for new workspaces).
  4. Pre-create workspaces explicitly with `tofu workspace new` to avoid the implicit-creation race.
Defensive patterns

Strategy: retry

Validate before calling

// preflight: check the -1 creation lock is free
var held bool
db.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_locks WHERE locktype='advisory' AND objid=-1::bigint)`).Scan(&held)
if held { return fmt.Errorf("workspace creation lock -1 is busy; retry shortly") }

Try / catch

for attempt := 0; attempt < 5; attempt++ {
    id, err := c.Lock(info)
    if err == nil { return id, nil }
    if !strings.Contains(err.Error(), "Already locked for workspace creation") { return "", err }
    time.Sleep(time.Duration(1<<attempt) * time.Second)
}

Prevention

When it happens

Trigger: Lock() is called, the SELECT finds no row for c.Name (workspace does not exist), and SELECT pg_try_advisory_lock(-1) returns 'false' - another session already holds the -1 creation lock. Returned as *statemgr.LockError.

Common situations: Two concurrent `tofu init` (or first apply) runs for two different brand-new workspaces; a previous init crashed while holding the -1 lock; CI matrix creates multiple workspaces simultaneously.

Related errors


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

Appendix: source

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

		return nil
	}

	// Try to acquire locks for the existing row `id` and the creation lock `-1`.
	query := `SELECT %s.id, pg_try_advisory_lock(%s.id), pg_try_advisory_lock(-1) FROM %s.%s WHERE %s.name = $1`
	row := c.Client.QueryRow(fmt.Sprintf(query, statesTableName, statesTableName, c.SchemaName, statesTableName, statesTableName), c.Name)
	var pgLockId, didLock, didLockForCreate []byte
	err = row.Scan(&pgLockId, &didLock, &didLockForCreate)
	switch {
	case err == sql.ErrNoRows:
		// No rows means we're creating the workspace. Take the creation lock.
		innerRow := c.Client.QueryRow(`SELECT pg_try_advisory_lock(-1)`)
		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

View on GitHub (pinned to d32a084675)