{"record":{"id":"d540c5e042df93ea","repo":"hashicorp/terraform","slug":"workspace-is-already-locked-s","errorCode":null,"errorMessage":"Workspace is already locked: %s","messagePattern":"Workspace is already locked: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/pg/client.go","lineNumber":120,"sourceCode":"\tswitch {\n\tcase err == sql.ErrNoRows:\n\t\t// No rows means we're creating the workspace. Take the creation lock.\n\t\tinnerRow := c.Client.QueryRow(`SELECT pg_try_advisory_lock(-1)`)\n\t\tvar innerDidLock []byte\n\t\terr := innerRow.Scan(&innerDidLock)\n\t\tif err != nil {\n\t\t\treturn \"\", &statemgr.LockError{Info: info, Err: err}\n\t\t}\n\t\tif string(innerDidLock) == \"false\" {\n\t\t\treturn \"\", &statemgr.LockError{Info: info, Err: fmt.Errorf(\"Already locked for workspace creation: %s\", c.Name)}\n\t\t}\n\t\tinfo.Path = \"-1\"\n\tcase err != nil:\n\t\treturn \"\", &statemgr.LockError{Info: info, Err: err}\n\tcase string(didLock) == \"false\":\n\t\t// Existing workspace is already locked. Release the attempted creation lock.\n\t\tlockUnlock(\"-1\")\n\t\treturn \"\", &statemgr.LockError{Info: info, Err: fmt.Errorf(\"Workspace is already locked: %s\", c.Name)}\n\tcase string(didLockForCreate) == \"false\":\n\t\t// Someone has the creation lock already. Release the existing workspace because it might not be safe to touch.\n\t\tlockUnlock(string(pgLockId))\n\t\treturn \"\", &statemgr.LockError{Info: info, Err: fmt.Errorf(\"Cannot lock workspace; already locked for workspace creation: %s\", c.Name)}\n\tdefault:\n\t\t// Existing workspace is now locked. Release the attempted creation lock.\n\t\tlockUnlock(\"-1\")\n\t\tinfo.Path = string(pgLockId)\n\t}\n\tc.info = info\n\n\treturn info.ID, nil\n}\n\nfunc (c *RemoteClient) getLockInfo() (*statemgr.LockInfo, error) {\n\treturn c.info, nil\n}\n","sourceCodeStart":102,"sourceCodeEnd":138,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/pg/client.go#L102-L138","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// preflight: check whether this workspace's advisory lock is held\nvar held bool\ndb.QueryRow(`SELECT EXISTS(SELECT 1 FROM pg_locks WHERE locktype='advisory' AND objid=$1)`, wsID).Scan(&held)\nif held { return fmt.Errorf(\"workspace %s is currently locked; retry or force-unlock\", name) }","typeGuard":"func isWorkspaceLockedErr(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"Workspace is already locked\")\n}","tryCatchPattern":"if _, err := c.Lock(info); err != nil {\n    var le *statemgr.LockError\n    if errors.As(err, &le) && le.Info != nil {\n        return fmt.Errorf(\"busy, held by %s since %s; run `tofu force-unlock %s`\", le.Info.Who, le.Info.Created, le.Info.ID)\n    }\n    return err\n}","preventionTips":["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."],"tags":["postgres","state-lock","advisory-lock","workspace","concurrency"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}