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
- Use the lock ID reported in the message's 'existing lock' value: `terraform force-unlock <existing-id>`.
- If you must break the lock intentionally, confirm ownership with the listed holder first, then force-unlock with the existing ID.
- Confirm the workspace matches the lock's state path (lockPath() = <bucket>/<path>).
- 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.
- 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
- Always force-unlock with the ID reported by Terraform.
- Do not run concurrent applies against the same workspace.
- Confirm the workspace matches the locked state path before unlocking.
- If unsure who owns the DDB lock, query the row's Info attribute and read ID/Operation/Who.
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
- lock ID ' ' does not match the existing lock ID
- failed to retrieve lock info for lock ID
- failed to unlock both S3 and DynamoDB: S3 error
- failed to unlock DynamoDB
- failed to clean up file lock after DynamoDB lock error
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)