hashicorp/terraform · error
Workspace is already locked
Error message
Workspace is already locked: %s
What it means
Thrown by RemoteClient.Lock in the pg backend when the workspace row exists but pg_try_advisory_lock(id) on its primary-key id returns false - meaning the advisory lock for that specific workspace is already held by another session. Before returning, the code releases the attempted -1 creation lock to avoid deadlocking creation. Returned as *statemgr.LockError.
Solutions
- Confirm no other apply is running on this workspace (`SELECT * FROM pg_locks WHERE locktype='advisory';`).
- Run `tofu force-unlock <lock-id>` if the holder is a dead session.
- Re-run apply after the other run completes.
- Add workspace-level locking in CI to prevent overlap.
Defensive patterns
Strategy: validation
Validate before calling
// preflight: check whether this workspace's advisory lock is held
var held bool
db.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_locks WHERE locktype='advisory' AND objid=$1)`, wsID).Scan(&held)
if held { return fmt.Errorf("workspace %s is currently locked; retry or force-unlock", name) } Type guard
func isWorkspaceLockedErr(err error) bool {
return err != nil && strings.Contains(err.Error(), "Workspace is already locked")
} Try / catch
if _, err := c.Lock(info); err != nil {
var le *statemgr.LockError
if errors.As(err, &le) && le.Info != nil {
return fmt.Errorf("busy, held by %s since %s; run `tofu force-unlock %s`", le.Info.Who, le.Info.Created, le.Info.ID)
}
return err
} Prevention
- Do not run overlapping applies on the same workspace.
- Always `tofu force-unlock` after a crashed apply before re-running.
- Surface LockError.Info to operators.
When it happens
Trigger: Lock() SELECTs the row, scans didLock as 'false' (the per-workspace lock is held), calls lockUnlock('-1') to release the creation lock, then returns. Triggered when a second `tofu apply` runs on an already-locked workspace.
Common situations: Concurrent applies to the same workspace; a previous apply crashed without releasing its lock; an interactive `tofu apply` is running in another terminal; CI overlap on the same workspace.
Related errors
- Already locked for workspace creation
- Cannot lock workspace; already locked for workspace creation
- error unlocking Postgres state
- failed to lock state in Postgres
- can't delete default state
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/d540c5e042df93ea.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/pg/client.go:120
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
return info.ID, nil
}
func (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {
return c.info, nil
}
View on GitHub (pinned to d32a084675)