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
- Retry once the in-flight workspace creation completes — the -1 lock is transient.
- Check pg_locks for advisory lock -1 and terminate the holder if stale.
- 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
- Avoid scheduling workspace creation concurrently with applies on the same backend.
- Clear stale -1 advisory locks via pg_terminate_backend.
- Document that creation locks are global across all workspaces.
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
- Already locked for workspace creation: %s
- Workspace is already locked: %s
- failed to lock state in Postgres: %s
- %s (lock ID: "%s/%s")
- failed to lock azure state: %s
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/aac5fc00f1fcf6dd.
Report an issue: GitHub.