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

  1. Pass the exact lock ID returned by the original Lock call (don't hand-edit it).
  2. If the original ID is lost, use the force-unlock path with the org/workspace string instead.
  3. 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

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


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)