hashicorp/terraform · error
lock ID does not match existing lock
Error message
lock ID does not match existing lock
What it means
On Unlock with a populated r.lockInfo, the supplied id is compared against the locally-tracked r.lockInfo.ID. A mismatch means the caller is trying to unlock with a different ID than the one returned by Lock — refused to avoid releasing someone else's lock. Returned inside a statemgr.LockError.
Solutions
- Pass the exact lock ID returned by the original Lock call (don't hand-edit it).
- If the original ID is lost, use the force-unlock path with the org/workspace string instead.
- Avoid concurrent unlock attempts on the same remoteClient instance.
Defensive patterns
Strategy: validation
Validate before calling
// Validate id matches the tracked lock before calling Unlock
if r.lockInfo != nil && r.lockInfo.ID != id {
return fmt.Errorf("refusing unlock: expected %s, got %s", r.lockInfo.ID, id)
} Type guard
func lockIdMatches(info *statemgr.LockInfo, id string) bool {
return info != nil && info.ID == id
} Prevention
- Always pass the exact ID returned by Lock.
- Don't share/persist a remoteClient across unrelated lock cycles.
- Use force-unlock with the org/workspace string when the original ID is lost.
When it happens
Trigger: r.lockInfo.ID != id: the process stored a lock ID, then Unlock was called with a different ID (e.g. copied from another workspace, or a stale id from a prior run).
Common situations: Manually invoking Unlock with a mismatched id; state persisted across processes but lockInfo not; concurrent attempts where one already updated lockInfo.
Related errors
- Error downloading state
- Error retrieving state
- lock ID does not match existing lock ID " /
- (lock ID: " / ")
- approved using the UI or API
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/3b31ec431b74d016.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote/backend_state.go:220
// Unlock the remote state.
func (r *remoteClient) Unlock(id string) error {
ctx := context.Background()
// We first check if there was an error while uploading the latest
// state. If so, we will not unlock the workspace to prevent any
// changes from being applied until the correct state is uploaded.
if r.stateUploadErr {
return nil
}
lockErr := &statemgr.LockError{Info: r.lockInfo}
// With lock info this should be treated as a normal unlock.
if r.lockInfo != nil {
// Verify the expected lock ID.
if r.lockInfo.ID != id {
lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
return lockErr
}
// Unlock the workspace.
// Unlock the workspace.
err := RetryBackoff(ctx, func() error {
_, err := r.client.Workspaces.Unlock(ctx, r.workspace.ID)
if err != nil {
if errors.Is(err, tfe.ErrWorkspaceLockedStateVersionStillPending) {
// This is a retryable error.
return err
}
// This will not be retried
return &errorUnlockFailed{innerError: err}
}
return nil
})
View on GitHub (pinned to d32a084675)