hashicorp/terraform · error

lock ID does not match existing lock ( )

Error message

lock ID %q does not match existing lock (%q)

What it means

Thrown inside unlockWithDynamoDB when the lock info was successfully read from DynamoDB but the stored lock ID does not match the id passed to Unlock. This is the DynamoDB counterpart of the S3 lock-ID guard: Terraform refuses to delete a row it does not own, protecting another operator's lock.

Solutions

  1. Use the lock ID reported in the message's 'existing lock' value: `terraform force-unlock <existing-id>`.
  2. If you must break the lock intentionally, confirm ownership with the listed holder first, then force-unlock with the existing ID.
  3. Confirm the workspace matches the lock's state path (lockPath() = <bucket>/<path>).
  4. If the existing ID is unknown, query the DDB row: `aws dynamodb get-item --table-name <table> --key '{...}' --projection-expression 'Info'` and read the ID from the Info JSON.
  5. Prevent concurrent applies against the same workspace to avoid ID churn.

Example fix

# read the current DDB lock owner, then force-unlock with that ID
aws dynamodb get-item \
  --table-name terraform-locks \
  --key '{"LockID":{"S":"tf-state-prod/prod/terraform.tfstate"}}' \
  --projection-expression 'Info'
# parse Info -> ID, then
terraform force-unlock <existing-id-from-info>
Defensive patterns

Strategy: validation

Validate before calling

// Before unlock, fetch the current DDB lock ID and compare.
func currentDDBLockID(ctx context.Context, c *dynamodb.Client, table, lockPath string) (string, error) {
  resp, err := c.GetItem(ctx, &dynamodb.GetItemInput{
    Key: map[string]types.AttributeValue{"LockID": &types.AttributeValueMemberS{Value: lockPath}},
    TableName: &table, ProjectionExpression: aws.String("Info"),
  })
  if err != nil { return "", err }
  if v, ok := resp.Item["Info"].(*types.AttributeValueMemberS); ok {
    var li statemgr.LockInfo
    if err := json.Unmarshal([]byte(v.Value), &li); err == nil { return li.ID, nil }
  }
  return "", nil
}

Type guard

func isDDBLockOwner(storedID, unlockID string) bool { return storedID != "" && storedID == unlockID }

Try / catch

// On mismatch, return the existing ID so the operator can force-unlock correctly.
if lockInfo.ID != id {
  return fmt.Errorf("lock ID %q does not match existing lock (%q); "+
    "run `terraform force-unlock %s` with the existing ID", id, lockInfo.ID, lockInfo.ID)
}

Prevention

When it happens

Trigger: lockInfo.ID != id at client.go:571. Triggers: force-unlock invoked with the wrong DDB lock ID, a newer run overwrote the DDB row with its own ID, stale lock ID from a prior cleared run, or operating against the wrong workspace whose DDB row holds a different ID.

Common situations: Pasting an outdated lock ID into force-unlock, a teammate re-locked after your run ended, wrong workspace selected, or the DDB row's Info JSON was rewritten by another client.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/s3/client.go:572

	}

	log.Debug(fmt.Sprintf("Deleted lock file: '%q'", c.lockFilePath))

	return nil
}

func (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {
	// TODO: store the path and lock ID in separate fields, and have proper
	// projection expression only delete the lock if both match, rather than
	// checking the ID from the info field first.
	lockInfo, err := c.getLockInfoWithDynamoDB(ctx)
	if err != nil {
		return fmt.Errorf("failed to retrieve lock info for lock ID %q: %s", id, err)
	}
	lockErr.Info = lockInfo

	if lockInfo.ID != id {
		return fmt.Errorf("lock ID %q does not match existing lock (%q)", id, lockInfo.ID)
	}

	params := &dynamodb.DeleteItemInput{
		Key: map[string]dynamodbtypes.AttributeValue{
			"LockID": &dynamodbtypes.AttributeValueMemberS{
				Value: c.lockPath(),
			},
		},
		TableName: aws.String(c.ddbTable),
	}
	_, err = c.dynClient.DeleteItem(ctx, params)

	if err != nil {
		return err
	}
	return nil
}

View on GitHub (pinned to d32a084675)