hashicorp/terraform · error

lock id mismatch, !=

Error message

lock id mismatch, %v != %v

What it means

remoteClient.Unlock(check) reads the lock file (lockInfo() sets info.ID to the stored md5 checksum) and compares it to the supplied check. A mismatch means the caller is trying to release a lock whose ID does not match the current lock object — i.e., the lock being unlocked is not the one the caller holds.

Solutions

  1. Obtain the current lock ID: read the lock object's X-Cos-Meta-Md5 / md5 from the bucket at c.lockFile (or from the error output of the locking operation).
  2. Run `terraform force-unlock <correct-ID>` with that ID.
  3. If no active run holds the lock, manually delete the lock object.
Defensive patterns

Strategy: validation

Validate before calling

// Verify the supplied lock ID matches the stored lock object before Unlock.
func verifyLockID(c *remoteClient, check string) error {
    info, err := c.lockInfo()
    if err != nil {
        return err
    }
    if info.ID != check {
        return fmt.Errorf("lock id mismatch, %v != %v", info.ID, check)
    }
    return nil
}

Try / catch

// On mismatch, re-read the stored lock ID and guide the user.
if err := c.Unlock(providedID); err != nil {
    if strings.Contains(err.Error(), "lock id mismatch") {
        info, lerr := c.lockInfo()
        if lerr == nil {
            return fmt.Errorf("wrong lock id; current id=%s. Run terraform force-unlock %s", info.ID, info.ID)
        }
    }
    return err
}

Prevention

When it happens

Trigger: Unlock(check) -> info.ID != check; lockError wraps the mismatch.

Common situations: User runs `terraform force-unlock` with a stale or wrong ID; the lock was lost (session/lock expired) and re-acquired by another run with a different md5; multiple locks in flight and the wrong one was targeted.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/cos/client.go:126

	err = c.putObject(c.lockFile, data)
	if err != nil {
		return "", c.lockError(err)
	}

	return check, nil
}

// Unlock unlock remote state file
func (c *remoteClient) Unlock(check string) error {
	log.Printf("[DEBUG] unlock remote state file %s", c.lockFile)

	info, err := c.lockInfo()
	if err != nil {
		return c.lockError(err)
	}

	if info.ID != check {
		return c.lockError(fmt.Errorf("lock id mismatch, %v != %v", info.ID, check))
	}

	err = c.deleteObject(c.lockFile)
	if err != nil {
		return c.lockError(err)
	}

	err = c.cosUnlock(c.bucket, c.lockFile)
	if err != nil {
		return c.lockError(err)
	}

	return nil
}

// lockError returns statemgr.LockError
func (c *remoteClient) lockError(err error) *statemgr.LockError {
	log.Printf("[DEBUG] failed to lock or unlock %s: %v", c.lockFile, err)

View on GitHub (pinned to d32a084675)