hashicorp/terraform · error · statemgr.LockError

lock ID '%s' does not match the existing lock ID '%s'

Error message

lock ID '%s' does not match the existing lock ID '%s'

What it means

Raised in Unlock when the lock ID supplied by the caller does not equal lockInfo.ID read from the existing lock file. This is a deliberate guard preventing one process from unlocking state that another process holds. It is returned as a statemgr.LockError carrying the current LockInfo so the caller can report who actually holds the lock.

Source

Thrown at internal/backend/remote-state/oci/client.go:337

	if err := json.Unmarshal(lockByteData, lockInfo); err != nil {
		return lockInfo, "", fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
	}
	return lockInfo, *getResponse.ETag, nil
}
func (c *RemoteClient) Unlock(id string) error {
	ctx := context.TODO()
	logger := logWithOperation("unlock-state-file").Named(c.lockFilePath)
	logger.Info("unlocking remote state")
	lockInfo, etag, err := c.getLockInfo(ctx)

	if err != nil {
		return fmt.Errorf("Failed to retrieve lock information from OCI Object Storage: %w", err)
	}
	// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
	if lockInfo.ID != id {
		return &statemgr.LockError{
			Info: lockInfo,
			Err:  fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID),
		}
	}

	deleteRequest := objectstorage.DeleteObjectRequest{
		NamespaceName: common.String(c.namespace),
		ObjectName:    common.String(c.lockFilePath),
		BucketName:    common.String(c.bucketName),
		IfMatch:       common.String(etag),
		RequestMetadata: common.RequestMetadata{
			RetryPolicy: getDefaultRetryPolicy(),
		},
	}
	deleteResponse, err := c.objectStorageClient.DeleteObject(ctx, deleteRequest)
	if err != nil {
		return &statemgr.LockError{
			Info: lockInfo,
			Err:  err,
		}

View on GitHub (pinned to c9def3e214)

Solutions

  1. Read the current lock file (oci os object get <lockfile>) to obtain the real lockInfo.ID and use that ID for force-unlock.
  2. Confirm via the LockInfo Who/Operation/Created fields that it is genuinely safe to unlock (no active run holds it).
  3. Only after confirming, delete the lock object directly if Terraform's unlock keeps refusing the ID.
  4. Avoid force-unlock when an apply is in progress; coordinate with the listed lock owner first.
Defensive patterns

Strategy: validation

Validate before calling

// Before Unlock, read the current lock file and obtain the real ID:
//   oci os object get --namespace <ns> --bucket-name <b> --name <lockfile> --file -
// Pass the JSON's "ID" to force-unlock, not a stale/copy-pasted id.

Type guard

// The lib returns a *statemgr.LockError carrying the real holder; use it:
var le *statemgr.LockError
if errors.As(err, &le) && le.Info != nil {
    // le.Info.ID is who really holds the lock; le.Info.Who/Operation/Path for context
}

Try / catch

if lockInfo.ID != id {
    return &statemgr.LockError{Info: lockInfo, Err: fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID)}
}

Prevention

When it happens

Trigger: Unlock(id) is called with an id that differs from lockInfo.ID at client.go:334. Happens when force-unlock is attempted with a stale/wrong ID, or when the lock was re-acquired by a different run after the caller's ID became stale.

Common situations: User runs 'terraform force-unlock <old-id>' after the lock was already released and re-acquired with a new ID; copy-pasting the wrong ID; two operators racing to unlock.

Related errors


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