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
- 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.
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
- 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.
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
- Unable to retrieve item from DynamoDB table
- Unable to delete item from DynamoDB table
- failed to clean up file lock after DynamoDB lock error
- failed to retrieve lock info for lock ID
- failed to unlock both S3 and DynamoDB: S3 error
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)