hashicorp/terraform · error
Unable to delete item from DynamoDB table
Error message
Unable to delete item from DynamoDB table %q: %w
What it means
Thrown by deleteMD5 when the DynamoDB DeleteItem that removes the state MD5 digest fails. Called from the backend's Delete path (after the S3 state object is deleted) to clean up the corresponding digest row. Failure here leaves an orphaned digest row pointing at a deleted state object.
Solutions
- Manually delete the orphaned digest row: `aws dynamodb delete-item --table-name <table> --key '{"LockID":{"S":"<bucket>/<path>-md5"}}'` (the stateIDSuffix is typically `-md5`).
- Verify dynamodb:DeleteItem permission on the table for the principal.
- Confirm the table still exists in-region; if recreated, update backend `dynamodb_table`.
- For write throttling, switch the table to on-demand or raise write capacity.
- Orphaned digest rows are low-risk but can cause stale-state false positives later; clear them to be safe.
Example fix
# remove the orphaned digest row after a failed deleteMD5
aws dynamodb delete-item \
--table-name terraform-locks \
--key '{"LockID":{"S":"tf-state-prod/prod/terraform.tfstate-md5"}}' Defensive patterns
Strategy: retry
Validate before calling
// Before deleting state, confirm DDB delete access.
func ddbDeleteReachable(ctx context.Context, c *dynamodb.Client, table string) error {
if _, err := c.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
return err
}
return nil
} Try / catch
// Retry transient DeleteItem; surface ResourceNotFound/AccessDenied distinctly.
for i := 0; i < 3; i++ {
if _, err := c.dynClient.DeleteItem(ctx, params); err == nil { return nil }
else {
var apiErr smithy.APIError
if errors.As(err, &apiErr) && (apiErr.ErrorCode() == "ResourceNotFoundException" || apiErr.ErrorCode() == "AccessDenied") {
return fmt.Errorf("Unable to delete item from DynamoDB table %q: %w", c.ddbTable, err)
}
time.Sleep(backoff(i))
}
} Prevention
- Grant dynamodb:DeleteItem on the lock table in the apply role.
- Clean up orphaned digest rows (LockID ending in stateIDSuffix) when manually removing state.
- Use on-demand billing to avoid write throttling during workspace deletion.
- Keep the table name stable across the state lifecycle.
When it happens
Trigger: c.dynClient.DeleteItem at client.go:670 returns an error. Triggers: dynamodb_table deleted between the S3 delete and this call, IAM principal lacks dynamodb:DeleteItem, table throttled on write capacity, or a resource policy denying the principal.
Common situations: Table dropped mid-cleanup, IAM permissions narrowed after the S3 delete succeeded, write-capacity throttling during bulk workspace deletion, or cross-account role missing DDB write perms.
Related errors
- failed to store state MD5
- Unable to retrieve item from DynamoDB table
- failed to retrieve lock info for lock ID
- failed to unlock DynamoDB
- error deleting state
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/6e9067f08c0c288a.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/s3/client.go:671
return nil
}
// remove the hash value for a deleted state
func (c *RemoteClient) deleteMD5(ctx context.Context) error {
if c.ddbTable == "" {
return nil
}
params := &dynamodb.DeleteItemInput{
Key: map[string]dynamodbtypes.AttributeValue{
"LockID": &dynamodbtypes.AttributeValueMemberS{
Value: c.lockPath() + stateIDSuffix,
},
},
TableName: aws.String(c.ddbTable),
}
if _, err := c.dynClient.DeleteItem(ctx, params); err != nil {
return fmt.Errorf("Unable to delete item from DynamoDB table %q: %w", c.ddbTable, err)
}
return nil
}
// getLockInfoWithFile retrieves and parses a lock file from an S3 bucket.
func (c *RemoteClient) getLockInfoWithFile(ctx context.Context) (*statemgr.LockInfo, error) {
// Attempt to retrieve the lock file from S3.
getOutput, err := c.s3Client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(c.lockFilePath),
})
if err != nil {
return nil, fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err)
}
defer func() {
if cerr := getOutput.Body.Close(); cerr != nil {
log.Printf("failed to close S3 object body: %v", cerr)
}View on GitHub (pinned to d32a084675)