{"record":{"id":"a3200381244e868e","repo":"hashicorp/terraform","slug":"lock-id-q-does-not-match-existing-lock-q","errorCode":null,"errorMessage":"lock ID %q does not match existing lock (%q)","messagePattern":"lock ID %q does not match existing lock \\(%q\\)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":572,"sourceCode":"\t}\n\n\tlog.Debug(fmt.Sprintf(\"Deleted lock file: '%q'\", c.lockFilePath))\n\n\treturn nil\n}\n\nfunc (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {\n\t// TODO: store the path and lock ID in separate fields, and have proper\n\t// projection expression only delete the lock if both match, rather than\n\t// checking the ID from the info field first.\n\tlockInfo, err := c.getLockInfoWithDynamoDB(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to retrieve lock info for lock ID %q: %s\", id, err)\n\t}\n\tlockErr.Info = lockInfo\n\n\tif lockInfo.ID != id {\n\t\treturn fmt.Errorf(\"lock ID %q does not match existing lock (%q)\", id, lockInfo.ID)\n\t}\n\n\tparams := &dynamodb.DeleteItemInput{\n\t\tKey: map[string]dynamodbtypes.AttributeValue{\n\t\t\t\"LockID\": &dynamodbtypes.AttributeValueMemberS{\n\t\t\t\tValue: c.lockPath(),\n\t\t\t},\n\t\t},\n\t\tTableName: aws.String(c.ddbTable),\n\t}\n\t_, err = c.dynClient.DeleteItem(ctx, params)\n\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n","sourceCodeStart":554,"sourceCodeEnd":590,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/s3/client.go#L554-L590","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# read the current DDB lock owner, then force-unlock with that ID\naws dynamodb get-item \\\n  --table-name terraform-locks \\\n  --key '{\"LockID\":{\"S\":\"tf-state-prod/prod/terraform.tfstate\"}}' \\\n  --projection-expression 'Info'\n# parse Info -> ID, then\nterraform force-unlock <existing-id-from-info>","handlingStrategy":"validation","validationCode":"// Before unlock, fetch the current DDB lock ID and compare.\nfunc currentDDBLockID(ctx context.Context, c *dynamodb.Client, table, lockPath string) (string, error) {\n  resp, err := c.GetItem(ctx, &dynamodb.GetItemInput{\n    Key: map[string]types.AttributeValue{\"LockID\": &types.AttributeValueMemberS{Value: lockPath}},\n    TableName: &table, ProjectionExpression: aws.String(\"Info\"),\n  })\n  if err != nil { return \"\", err }\n  if v, ok := resp.Item[\"Info\"].(*types.AttributeValueMemberS); ok {\n    var li statemgr.LockInfo\n    if err := json.Unmarshal([]byte(v.Value), &li); err == nil { return li.ID, nil }\n  }\n  return \"\", nil\n}","typeGuard":"func isDDBLockOwner(storedID, unlockID string) bool { return storedID != \"\" && storedID == unlockID }","tryCatchPattern":"// On mismatch, return the existing ID so the operator can force-unlock correctly.\nif lockInfo.ID != id {\n  return fmt.Errorf(\"lock ID %q does not match existing lock (%q); \"+\n    \"run `terraform force-unlock %s` with the existing ID\", id, lockInfo.ID, lockInfo.ID)\n}","preventionTips":["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."],"tags":["locking","dynamodb","remote-state","lock-id","safety-guard","concurrency","unlock"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}