{"record":{"id":"3b31ec431b74d016","repo":"hashicorp/terraform","slug":"lock-id-does-not-match-existing-lock","errorCode":null,"errorMessage":"lock ID does not match existing lock","messagePattern":"lock ID does not match existing lock","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote/backend_state.go","lineNumber":220,"sourceCode":"\n// Unlock the remote state.\nfunc (r *remoteClient) Unlock(id string) error {\n\tctx := context.Background()\n\n\t// We first check if there was an error while uploading the latest\n\t// state. If so, we will not unlock the workspace to prevent any\n\t// changes from being applied until the correct state is uploaded.\n\tif r.stateUploadErr {\n\t\treturn nil\n\t}\n\n\tlockErr := &statemgr.LockError{Info: r.lockInfo}\n\n\t// With lock info this should be treated as a normal unlock.\n\tif r.lockInfo != nil {\n\t\t// Verify the expected lock ID.\n\t\tif r.lockInfo.ID != id {\n\t\t\tlockErr.Err = fmt.Errorf(\"lock ID does not match existing lock\")\n\t\t\treturn lockErr\n\t\t}\n\n\t\t// Unlock the workspace.\n\t\t// Unlock the workspace.\n\t\terr := RetryBackoff(ctx, func() error {\n\t\t\t_, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID)\n\t\t\tif err != nil {\n\t\t\t\tif errors.Is(err, tfe.ErrWorkspaceLockedStateVersionStillPending) {\n\t\t\t\t\t// This is a retryable error.\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// This will not be retried\n\t\t\t\treturn &errorUnlockFailed{innerError: err}\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n","sourceCodeStart":202,"sourceCodeEnd":238,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote/backend_state.go#L202-L238","documentation":"Emitted by remoteClient.Unlock (backend_state.go:217-222) on the normal-unlock path where the client holds r.lockInfo but the provided `id` does not equal r.lockInfo.ID. This guards against unlocking with the wrong lock identity — the recorded lock and the requested unlock ID must agree before the Workspaces.Unlock API is called.","triggerScenarios":"Two parts of the same process (or a wrapper) attempt to unlock with different lock IDs; the lockInfo was populated by a different lock acquisition than the one being released; programmatic misuse passing a stale or fabricated lock ID to Unlock.","commonSituations":"A custom automation layer caches a lock ID from a previous run and passes it to a new run's unlock; concurrent goroutines unlocking the same remoteClient; mismatch between the ID returned by Lock() and the id passed to Unlock().","solutions":["Thread the exact string returned by Lock() through to the matching Unlock() call without modification.","Ensure only one Unlock call per Lock; serialize lock/unlock per remoteClient instance.","If the recorded lockInfo is stale, use the force-unlock path (pass organization/workspace as id) instead of the normal path.","Audit the wrapper code to confirm it does not cache/reuse lock IDs across operations."],"exampleFix":"// before: passing a cached/guessed lock id\nlockID := cachedID            // wrong id\nerr := client.Unlock(lockID)  // \"lock ID does not match existing lock\"\n\n// after: use the id returned by Lock\nlockID, _ := client.Lock(info)\nerr := client.Unlock(lockID)  // matches r.lockInfo.ID","handlingStrategy":"validation","validationCode":"// Validate the unlock id matches the recorded lock before calling the API.\nif r.lockInfo != nil && r.lockInfo.ID != id {\n    return fmt.Errorf(\"lock ID does not match existing lock; recorded %q, got %q\", r.lockInfo.ID, id)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Thread the exact string returned by Lock() through to the matching Unlock() call.","Serialize lock/unlock per remoteClient instance; never share across concurrent operations.","Never cache/reuse lock IDs across runs.","If the recorded lock is stale, use the force-unlock path with the canonical org/workspace id."],"tags":["state","lock","unlock","concurrency"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}