hashicorp/terraform · error
lock ID %q does not match existing lock (%q)
Error message
lock ID %q does not match existing lock (%q)
What it means
Returned by unlockWithDynamoDB after the lock info is read from DynamoDB but the stored lockInfo.ID does not match the id passed to Unlock. This is the DynamoDB equivalent of the S3 ownership check (387) and prevents releasing another client's lock.
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 c9def3e214)
Solutions
- Use the most recent lock ID from the current 'Error acquiring the state lock' message.
- Query the table to read the live lockInfo.ID: aws dynamodb get-item with ProjectionExpression Info.
- Coordinate to avoid concurrent runs against the same workspace.
- If the stored lock is definitively orphaned, delete the row manually with the correct LockID key.
Example fix
# before: stale id
terraform force-unlock old-id
# after: read current id then unlock
aws dynamodb get-item --table-name locks --key '{"LockID":{"S":"<bucket>/<path>"}}'
terraform force-unlock <current-id> Defensive patterns
Strategy: validation
Validate before calling
// Read the live DynamoDB lock ID and compare before unlock
func liveDynamoLockID(ctx context.Context, ddbs *dynamodb.Client, table, lockPath string) (string, error) {
resp, err := ddbs.GetItem(ctx, &dynamodb.GetItemInput{
Key: map[string]types.AttributeValue{"LockID": &types.AttributeValueMemberS{Value: lockPath}},
ProjectionExpression: aws.String("Info"),
TableName: &table, ConsistentRead: aws.Bool(true),
})
if err != nil { return "", err }
var li statemgr.LockInfo
if s, ok := resp.Item["Info"].(*types.AttributeValueMemberS); ok {
if err := json.Unmarshal([]byte(s.Value), &li); err != nil { return "", err }
}
return li.ID, nil
} Try / catch
if err := client.Unlock(id); err != nil {
if strings.Contains(err.Error(), "does not match existing lock") {
// re-read live id and re-issue force-unlock
}
} Prevention
- Always use the lock ID from the most recent error message.
- Serialize CI runs per workspace to avoid ID churn.
- Read the live ID before force-unlock.
- Do not let external scripts edit the Info attribute.
When it happens
Trigger: Unlock(id) -> unlockWithDynamoDB where lockInfo.ID (from the Info attribute) != id: operator used a stale/wrong force-unlock ID, or another run re-acquired the lock after the current id was captured.
Common situations: Stale 'terraform force-unlock' id from an old error message; multiple CI runners racing on the same workspace; manual edits to the DynamoDB Info attribute.
Related errors
- failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoD
- failed to unlock DynamoDB: %v
- lock ID '%s' does not match the existing lock ID '%s'
- failed to retrieve lock info for lock ID %q: %s
- lock id %q does not match existing lock
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/a3200381244e868e.
Report an issue: GitHub.