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

  1. Grant dynamodb:DeleteItem on the table ARN.
  2. Verify the table still exists and the backend points to it.
  3. Scale write capacity if throttled.
  4. 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

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


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/6e9067f08c0c288a. Report an issue: GitHub.