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

  1. Manually delete the orphaned digest row: `aws dynamodb delete-item --table-name <table> --key '{"LockID":{"S":"<bucket>/<path>-md5"}}'` (the stateIDSuffix is typically `-md5`).
  2. Verify dynamodb:DeleteItem permission on the table for the principal.
  3. Confirm the table still exists in-region; if recreated, update backend `dynamodb_table`.
  4. For write throttling, switch the table to on-demand or raise write capacity.
  5. 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

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


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)