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
- Wait for the other workspace-creation run to finish, then retry.
- If no creation is actually in progress, clear the stuck lock: `SELECT pg_advisory_unlock(-1);` from psql.
- Serialize workspace creation in CI (one job at a time for new workspaces).
- 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
- Serialize new-workspace creation in CI.
- Pre-create workspaces with `tofu workspace new` ahead of apply.
- Clear stale -1 locks with `SELECT pg_advisory_unlock(-1);` after crashed inits.
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
- Cannot lock workspace; already locked for workspace creation
- Workspace is already locked
- error unlocking Postgres state
- failed to lock state in Postgres
- Error locking state
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 = infoView on GitHub (pinned to d32a084675)