hashicorp/terraform · error

failed to store state MD5

Error message

failed to store state MD5: %w

What it means

After the state object is successfully uploaded to S3, the backend writes the state's MD5 digest into DynamoDB (putMD5) so other clients can detect stale locally-cached state. This error wraps the putMD5 failure. The code comment is explicit: the next Get will inevitably fail because the digest is out of sync, so the whole Put is treated as failed even though the S3 object landed.

Solutions

  1. Confirm the DynamoDB table still exists and matches backend config: `aws dynamodb describe-table --table-name <ddbTable>` in the same region/profile.
  2. Verify the IAM principal has dynamodb:PutItem on arn:aws:dynamodb:<region>:<acct>:table/<ddbTable>; check both the role policy and any table resource-based policy.
  3. If the table was recreated, update backend `dynamodb_table` to the new name and re-run; the orphaned digest row in the old table is harmless.
  4. For throttling, switch the table to on-demand billing (PAY_PER_REQUEST) or raise write capacity, then retry the apply.
  5. If access is the issue and cannot be fixed immediately, you can drop `dynamodb_table` from config (disabling stale-state tracking) as a temporary measure, accepting the loss of stale-state detection.

Example fix

# before: stale ddb_table name after recreate
backend "s3" {
  bucket         = "tf-state-prod"
  dynamodb_table = "terraform-locks-old"
}
# after
backend "s3" {
  bucket         = "tf-state-prod"
  dynamodb_table = "terraform-locks"   # current live table
  region         = "us-west-2"
}
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight: confirm the DynamoDB lock table is reachable before Put.
func canWriteDigest(ctx context.Context, c *dynamodb.Client, table string) error {
  if _, err := c.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
    return fmt.Errorf("dynamodb table %s not reachable: %w", table, err)
  }
  return nil
}
// Run during backend Configure to fail fast instead of mid-apply.

Try / catch

// Retry transient DDB PutItem; fail fast on ResourceNotFound/AccessDenied.
for i := 0; i < 3; i++ {
  if _, err := dynClient.PutItem(ctx, putParams); err == nil {
    return nil
  } else {
    var apiErr smithy.APIError
    if errors.As(err, &apiErr) {
      if apiErr.ErrorCode() == "ResourceNotFoundException" || apiErr.ErrorCode() == "AccessDenied" {
        return fmt.Errorf("failed to store state MD5: %w", err)
      }
    }
    time.Sleep(backoff(i))
  }
}

Prevention

When it happens

Trigger: c.putMD5 at client.go:243 returns an error. Triggers: dynamodb_table is configured but the table was deleted/renamed, the IAM principal lacks dynamodb:PutItem on the table, the table is in a different region/account than the client, DynamoDB is throttled/provisioned-capacity exhausted, or the table ARN is correct but a resource policy denies the principal.

Common situations: Someone deleted or recreated the DynamoDB lock table without updating backend config, cross-account role that grants S3 but not DynamoDB access, switched AWS profiles so S3 writes succeed under one account but DDB writes hit another, or heavy concurrent applies exhausting provisioned write capacity on the lock table.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/9e165bfa15b5c0c0. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/s3/client.go:246

	if c.acl != "" {
		input.ACL = s3types.ObjectCannedACL(c.acl)
	}

	log.Info("Uploading remote state")

	uploader := manager.NewUploader(c.s3Client, func(u *manager.Uploader) {
		u.ClientOptions = optFns
	})
	_, err := uploader.Upload(ctx, input)
	if err != nil {
		return fmt.Errorf("failed to upload state: %w", err)
	}

	if err := c.putMD5(ctx, sum[:]); err != nil {
		// if this errors out, we unfortunately have to error out altogether,
		// since the next Get will inevitably fail.
		return fmt.Errorf("failed to store state MD5: %w", err)
	}

	return nil
}

func (c *RemoteClient) Delete() tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	ctx := context.TODO()
	log := c.logger(operationClientDelete)

	ctx, baselog := baselogging.NewHcLogger(ctx, log)
	ctx = baselogging.RegisterLogger(ctx, baselog)

	log.Info("Deleting remote state")

	_, err := c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(c.bucketName),
		Key:    aws.String(c.path),

View on GitHub (pinned to d32a084675)