hashicorp/terraform · error · statemgr.LockError
Workspace is already locked: %s
Error message
Workspace is already locked: %s
What it means
Returned by RemoteClient.Lock (pg/client.go:120) for an existing workspace. The row was found but pg_try_advisory_lock(<workspace id>) returned false, meaning another session already holds the per-workspace advisory lock. This is the standard, expected lock-contention error for the Postgres backend.
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 c9def3e214)
Solutions
- Coordinate with the other run and wait for it to finish, then retry.
- If the holder is dead, use `terraform force-unlock <lock-id>` — though for Postgres advisory locks this typically means terminating the holding DB backend.
- Inspect pg_locks (objid = workspace id) and pg_stat_activity to identify the holder.
Defensive patterns
Strategy: try-catch
Validate before calling
// Detect an existing holder before locking // SELECT pid FROM pg_locks l JOIN pg_stat_activity a USING (pid) // WHERE l.locktype='advisory' AND l.objid=<workspaceId>;
Type guard
// Per-workspace lock contention returns a typed error
// var lockErr *statemgr.LockError
// if errors.As(err, &lockErr) {
// who := lockErr.Info // operation, who, created
// } Try / catch
// Standard lock-contention handling: surface who holds it, offer force-unlock
// _, err := client.Lock(info)
// var le *statemgr.LockError
// if errors.As(err, &le) {
// fmt.Printf("locked by %s since %s; use terraform force-unlock if stale\n", le.Info.Who, le.Info.Created)
// } Prevention
- Run one apply per workspace at a time.
- Ensure killed Terraform processes release their Postgres sessions promptly.
- Alert on advisory locks held longer than the expected apply window.
When it happens
Trigger: Concurrent `terraform apply`/plan/refresh on the same workspace; a previous run crashed but its Postgres connection still holds the advisory lock.
Common situations: Two developers or two CI runs targeting the same workspace; a long apply whose connection lingered; a killed process whose pooler kept the session alive.
Related errors
- Already locked for workspace creation: %s
- Cannot lock workspace; already locked for workspace creation
- 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/d540c5e042df93ea.
Report an issue: GitHub.