hashicorp/terraform · error · statemgr.LockError

lock ID does not match existing lock

Error message

lock ID does not match existing lock

What it means

In Unlock's normal path (when s.lockInfo is set), the provided id is compared against the internally recorded s.lockInfo.ID. If they differ, unlock is refused so a caller cannot release a lock it does not own.

Source

Thrown at internal/cloud/state.go:475

		return nil
	}

	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 s.stateUploadErr {
		return nil
	}

	lockErr := &statemgr.LockError{Info: s.lockInfo}

	// With lock info this should be treated as a normal unlock.
	if s.lockInfo != nil {
		// Verify the expected lock ID.
		if s.lockInfo.ID != id {
			lockErr.Err = fmt.Errorf("lock ID does not match existing lock")
			return lockErr
		}

		// Unlock the workspace.
		err := RetryBackoff(ctx, func() error {
			_, err := s.tfeClient.Workspaces.Unlock(ctx, s.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
		})

		if err != nil {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Pass the exact lock ID returned by the corresponding Lock call.
  2. If the real lock ID is unknown, use the force-unlock path with the "<org>/<workspace>" ID.
  3. Ensure only the process that acquired the lock releases it.

Example fix

// before: unlocking with the wrong id
state.Unlock(someOtherID)

// after: unlock with the id returned by Lock
lockID, _ := state.Lock(info)
state.Unlock(lockID)
Defensive patterns

Strategy: validation

Validate before calling

// Validate the lock ID matches the one we hold before unlocking
if s.lockInfo != nil && s.lockInfo.ID != providedID {
    return fmt.Errorf("refusing unlock: expected %q, got %q", s.lockInfo.ID, providedID)
}

Try / catch

if err := state.Unlock(lockID); err != nil {
    var lockErr *statemgr.LockError
    if errors.As(err, &lockErr) && strings.Contains(lockErr.Err.Error(), "does not match") {
        // use the correct ID from Lock(), or the force-unlock path
    }
}

Prevention

When it happens

Trigger: Unlock is called with a lock ID that was not the one returned by the matching Lock call; e.g. two state managers sharing state incorrectly, or Unlock called twice with different IDs.

Common situations: Programmatic misuse of the statemgr.Full interface, or stale lock info after a restart trying to unlock with the wrong handle.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/bf308b322a5b526d. Report an issue: GitHub.