{"record":{"id":"9e165bfa15b5c0c0","repo":"hashicorp/terraform","slug":"failed-to-store-state-md5-w","errorCode":null,"errorMessage":"failed to store state MD5: %w","messagePattern":"failed to store state MD5: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":246,"sourceCode":"\n\tif c.acl != \"\" {\n\t\tinput.ACL = s3types.ObjectCannedACL(c.acl)\n\t}\n\n\tlog.Info(\"Uploading remote state\")\n\n\tuploader := manager.NewUploader(c.s3Client, func(u *manager.Uploader) {\n\t\tu.ClientOptions = optFns\n\t})\n\t_, err := uploader.Upload(ctx, input)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to upload state: %w\", err)\n\t}\n\n\tif err := c.putMD5(ctx, sum[:]); err != nil {\n\t\t// if this errors out, we unfortunately have to error out altogether,\n\t\t// since the next Get will inevitably fail.\n\t\treturn fmt.Errorf(\"failed to store state MD5: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *RemoteClient) Delete() tfdiags.Diagnostics {\n\tvar diags tfdiags.Diagnostics\n\tctx := context.TODO()\n\tlog := c.logger(operationClientDelete)\n\n\tctx, baselog := baselogging.NewHcLogger(ctx, log)\n\tctx = baselogging.RegisterLogger(ctx, baselog)\n\n\tlog.Info(\"Deleting remote state\")\n\n\t_, err := c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{\n\t\tBucket: aws.String(c.bucketName),\n\t\tKey:    aws.String(c.path),","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/s3/client.go#L228-L264","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Confirm the DynamoDB table still exists and matches backend config: `aws dynamodb describe-table --table-name <ddbTable>` in the same region/profile.","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.","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.","For throttling, switch the table to on-demand billing (PAY_PER_REQUEST) or raise write capacity, then retry the apply.","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."],"exampleFix":"# before: stale ddb_table name after recreate\nbackend \"s3\" {\n  bucket         = \"tf-state-prod\"\n  dynamodb_table = \"terraform-locks-old\"\n}\n# after\nbackend \"s3\" {\n  bucket         = \"tf-state-prod\"\n  dynamodb_table = \"terraform-locks\"   # current live table\n  region         = \"us-west-2\"\n}","handlingStrategy":"retry","validationCode":"// Pre-flight: confirm the DynamoDB lock table is reachable before Put.\nfunc canWriteDigest(ctx context.Context, c *dynamodb.Client, table string) error {\n  if _, err := c.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {\n    return fmt.Errorf(\"dynamodb table %s not reachable: %w\", table, err)\n  }\n  return nil\n}\n// Run during backend Configure to fail fast instead of mid-apply.","typeGuard":null,"tryCatchPattern":"// Retry transient DDB PutItem; fail fast on ResourceNotFound/AccessDenied.\nfor i := 0; i < 3; i++ {\n  if _, err := dynClient.PutItem(ctx, putParams); err == nil {\n    return nil\n  } else {\n    var apiErr smithy.APIError\n    if errors.As(err, &apiErr) {\n      if apiErr.ErrorCode() == \"ResourceNotFoundException\" || apiErr.ErrorCode() == \"AccessDenied\" {\n        return fmt.Errorf(\"failed to store state MD5: %w\", err)\n      }\n    }\n    time.Sleep(backoff(i))\n  }\n}","preventionTips":["Treat the DynamoDB lock table as infrastructure: manage it with Terraform and apply it before the state-backend config references it.","Keep `dynamodb_table` in version control alongside bucket and region; never edit it ad hoc.","Grant dynamodb:PutItem + GetItem + DeleteItem on the table ARN in the apply role.","Prefer on-demand billing for the lock table to avoid write throttling during concurrent applies."],"tags":["dynamodb","remote-state","aws","iam","md5","stale-state","consistency"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}