hashicorp/terraform · error

lock id %q does not match existing lock

Error message

lock id %q does not match existing lock

What it means

Unlock() requires the caller's lock id to match the lease's current HolderIdentity. When they differ, a LockError is returned (not a plain error) carrying the existing lock info, so Terraform can surface the real holder and guide a force-unlock decision.

Source

Thrown at internal/backend/remote-state/kubernetes/client.go:322

	}

	lease, err := c.getLease(leaseName)
	if err != nil {
		return err
	}

	if lease.Spec.HolderIdentity == nil {
		return fmt.Errorf("state is already unlocked")
	}

	lockInfo, err := c.getLockInfo(lease)
	if err != nil {
		return err
	}

	lockErr := &statemgr.LockError{Info: lockInfo}
	if *lease.Spec.HolderIdentity != id {
		lockErr.Err = fmt.Errorf("lock id %q does not match existing lock", id)
		return lockErr
	}

	lease.Spec.HolderIdentity = nil
	removeLockInfo(lease)

	_, err = c.kubernetesLeaseClient.Update(context.Background(), lease, metav1.UpdateOptions{})
	if err != nil {
		lockErr.Err = err
		return lockErr
	}

	return nil
}

func (c *RemoteClient) getLockInfo(lease *coordinationv1.Lease) (*statemgr.LockInfo, error) {
	lockData, ok := getLockInfo(lease)
	if len(lockData) == 0 || !ok {

View on GitHub (pinned to c9def3e214)

Solutions

  1. Re-read the current lock info from the lease (the error's LockError.Info carries it) and use that Lock ID to unlock.
  2. If the current holder is an abandoned run, run `terraform force-unlock <current-lock-id>` with the correct id.
  3. Avoid unlocking with cached/old lock IDs; always derive the id from the live lease.

Example fix

# before - unlocking with a stale lock ID captured earlier
terraform force-unlock 2024-01-01-old-run-id

# after - use the lock ID from the live lease shown in the error
terraform force-unlock 2024-08-07-current-run-id
Defensive patterns

Strategy: validation

Validate before calling

// Always read the current holder from the lease before unlocking
lease, err := c.getLease(name)
if err != nil { return err }
if lease.Spec.HolderIdentity == nil { return nil }
current := *lease.Spec.HolderIdentity
if current != id {
    // use `current` for force-unlock instead of the stale `id`
}
return c.Unlock(current)

Type guard

func lockIDMatches(lease *coordinationv1.Lease, id string) bool {
    return lease != nil &&
        lease.Spec.HolderIdentity != nil &&
        *lease.Spec.HolderIdentity == id
}

Prevention

When it happens

Trigger: Calling Unlock(id) at client.go:320-324 where *lease.Spec.HolderIdentity != id - the id passed (typically from an old run's lock info) does not equal the lease's current holder.

Common situations: Trying to unlock with a stale lock ID from a previous run after the lock was re-acquired by a newer run; a teammate force-unlocked and re-locked under a new id; an expired lease that was re-acquired; copy-pasting the wrong lock ID into force-unlock.

Related errors


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