{"record":{"id":"c30b1b6fa1ab2c02","repo":"hashicorp/terraform","slug":"the-state-is-already-locked-by-another-terraform-c","errorCode":null,"errorMessage":"the state is already locked by another terraform client","messagePattern":"the state is already locked by another terraform client","errorType":"exception","errorClass":"LockError","httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/kubernetes/client.go","lineNumber":285,"sourceCode":"\t\t\treturn \"\", err\n\t\t} else {\n\t\t\treturn info.ID, nil\n\t\t}\n\t}\n\n\tif lease.Spec.HolderIdentity != nil {\n\t\tif *lease.Spec.HolderIdentity == info.ID {\n\t\t\treturn info.ID, nil\n\t\t}\n\n\t\tcurrentLockInfo, err := c.getLockInfo(lease)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tlockErr := &statemgr.LockError{\n\t\t\tInfo: currentLockInfo,\n\t\t\tErr:  errors.New(\"the state is already locked by another terraform client\"),\n\t\t}\n\t\treturn \"\", lockErr\n\t}\n\n\tlease.Spec.HolderIdentity = pointer.StringPtr(info.ID)\n\tsetLockInfo(lease, info.Marshal())\n\t_, err = c.kubernetesLeaseClient.Update(ctx, lease, metav1.UpdateOptions{})\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn info.ID, err\n}\n\nfunc (c *RemoteClient) Unlock(id string) error {\n\tleaseName, err := c.createLeaseName()\n\tif err != nil {\n\t\treturn err","sourceCodeStart":267,"sourceCodeEnd":303,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/kubernetes/client.go#L267-L303","documentation":"Returned by RemoteClient.Lock (internal/backend/remote-state/kubernetes/client.go:285) wrapped in a statemgr.LockError when a coordination.k8s.io Lease already exists for the state and its HolderIdentity differs from info.ID. The k8s backend implements locking via a Lease per workspace; a held lease with a different holder means another terraform client owns the lock.","triggerScenarios":"A second 'terraform apply'/'plan' runs while the first still holds the Lease; a previous terraform process crashed without releasing the Lease (stale holder); the same process re-locking with a new random info.ID after losing track of the old one.","commonSituations":"CI runners sharing a namespace and workspace where one pipeline hangs; a crashed/killed terraform leaving an orphaned Lease; manual edits to the Lease; concurrent developers hitting the same k8s state backend.","solutions":["Wait for the other operation to finish and release the Lease, then retry.","If the holder is stale (confirmed no active run), run 'terraform force-unlock <lock-id>' to delete the Lease.","Manually delete the Lease object (kubectl delete lease <lock-tfstate-...>) only if you are certain no run is active.","Investigate why the previous terraform exited without unlocking (OOM-kill, SIGKILL)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"lockID, err := client.Lock(info)\nif err != nil {\n    var le *statemgr.LockError\n    if errors.As(err, &le) {\n        return fmt.Errorf(\"state already locked by %s (op %s) at %s; run 'terraform force-unlock %s' if stale\",\n            le.Info.Who, le.Info.Operation, le.Info.Created, le.Info.ID)\n    }\n    return err\n}","preventionTips":["Detect statemgr.LockError and present the holder info + force-unlock ID to users.","Coordinate CI pipelines so they don't run concurrent applies against the same workspace.","Investigate crashed/killed terraform processes that leave orphaned Leases."],"tags":["backend","kubernetes","locking","lease","distributed-lock"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}