hashicorp/terraform · warning

state is already unlocked

Error message

state is already unlocked

What it means

Returned by the k8s backend Unlock when the Lease's HolderIdentity is nil (client.go:311-313). A nil holder means no one currently holds the lock, so there is nothing to unlock. This is a plain error (not a LockError) because there is no lock info to attach.

Solutions

  1. Treat as benign if state is expected to be unlocked — verify with kubectl get lease <name> -o yaml.
  2. Ensure Unlock is called exactly once per acquired lock; guard against double-unlock in your code.
  3. If a real lock should exist, investigate what cleared HolderIdentity (manual edit, concurrent run).
  4. Avoid force-unlock unless the Lease truly shows a holder you cannot release.
Defensive patterns

Strategy: try-catch

Validate before calling

// Check the Lease holder before attempting unlock:
// lease, _ := getLease(name)
// if lease.Spec.HolderIdentity == nil { /* already unlocked, skip */ }

Try / catch

// if err := client.Unlock(id); err != nil {
//   if strings.Contains(err.Error(), "state is already unlocked") { /* benign */ }
// }

Prevention

When it happens

Trigger: Calling Unlock(id) when the state was never locked, or was already unlocked by a prior call, or the Lease was manually cleared. Also after a force-unlock that nilled HolderIdentity.

Common situations: Terraform retrying an unlock after a partial failure that already cleared the holder; manual kubectl editing of the Lease; a prior run that unlocked but did not finish cleanly; calling unlock with a stale lock ID after someone else unlocked.

Related errors


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

Appendix: source

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

		return "", err
	}

	return info.ID, err
}

func (c *RemoteClient) Unlock(id string) error {
	leaseName, err := c.createLeaseName()
	if err != nil {
		return err
	}

	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 {

View on GitHub (pinned to d32a084675)