hashicorp/terraform · warning
Unable to delete item from DynamoDB table %q: %w
Error message
Unable to delete item from DynamoDB table %q: %w
What it means
Thrown by deleteMD5 when DynamoDB DeleteItem on the digest row (LockID = lockPath()+stateIDSuffix) fails. deleteMD5 is called when a state is destroyed/removed to clean up the stored MD5 digest; failure leaves a stale digest that may trigger false stale-state warnings later.
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 c9def3e214)
Solutions
- Grant dynamodb:DeleteItem on the table ARN.
- Verify the table still exists and the backend points to it.
- Scale write capacity if throttled.
- Manually delete the orphaned digest row if cleanup keeps failing.
Example fix
// before: missing DeleteItem
// after
{
"Effect": "Allow",
"Action": "dynamodb:DeleteItem",
"Resource": "arn:aws:dynamodb:*:*:table/terraform-locks"
} Defensive patterns
Strategy: try-catch
Validate before calling
// Confirm DeleteItem is granted on the digest row
func canDeleteDigest(ctx context.Context, ddbs *dynamodb.Client, table, digestKey string) error {
// probe with a throwaway row, not the real one
probe := digestKey + "-probe"
if _, err := ddbs.PutItem(ctx, &dynamodb.PutItemInput{TableName: &table, Item: map[string]types.AttributeValue{"LockID": &types.AttributeValueMemberS{Value: probe}}}); err != nil { return err }
_, err := ddbs.DeleteItem(ctx, &dynamodb.DeleteItemInput{TableName: &table, Key: map[string]types.AttributeValue{"LockID": &types.AttributeValueMemberS{Value: probe}}})
return err
} Try / catch
if err := client.deleteMD5(ctx); err != nil {
// stale-digest cleanup failure is non-fatal; log and continue
log.Printf("warn: digest cleanup failed (table may need manual cleanup): %v", err)
} Prevention
- Grant dynamodb:DeleteItem on the lock table.
- Treat deleteMD5 failure as warning, not fatal — the putMD5 still keeps state fresh.
- Periodically reap orphaned digest rows.
- Keep the table in sync with the backend config.
When it happens
Trigger: A state-destroy or workspace-deletion path calls deleteMD5 -> c.dynClient.DeleteItem returns an error: AccessDenied, table deleted mid-operation, throttling, or item-not-found under a conditional expression.
Common situations: IAM principal has GetItem/PutItem but not DeleteItem; table removed before cleanup; heavy concurrency hitting provisioned write limits; SCP denying deletes.
Related errors
- failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoD
- failed to unlock DynamoDB: %v
- failed to retrieve lock info for lock ID %q: %s
- Unable to retrieve item from DynamoDB table %q: %w
- failed to unlock S3: %v
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/6e9067f08c0c288a.
Report an issue: GitHub.