hashicorp/terraform · error

failed to store state MD5

Error message

failed to store state MD5: %s

What it means

Thrown by RemoteClient.Put after a successful OSS PutObject: writing the state's MD5 digest to Tablestore (c.putMD5) failed. Because the next Get validates against this digest, the backend aborts the whole Put rather than leave an unverifiable state.

Solutions

  1. Confirm the OTS table configured for the backend still exists and credentials have ots:PutRow.
  2. Verify OTS endpoint reachability from the runner.
  3. Re-run terraform apply once OTS is healthy — OSS already has the new state, only the digest needs to catch up.
  4. If MD5 row conflicts, ensure the state key path is unique per workspace.
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: confirm the OTS table accepts PutRow for the MD5 primary key.
if _, err := c.otsClient.PutRow(md5Probe); err != nil {
    return fmt.Errorf("OTS MD5 write pre-flight failed: %w", err)
}

Try / catch

// If MD5 write fails after OSS PutObject, retry the MD5 write a few times before aborting.
for i := 0; i < 3; i++ {
    if err := c.putMD5(sum[:]); err == nil { return diags }
    time.Sleep(backoff); backoff *= 2
}
return diags.Append(fmt.Errorf("failed to store state MD5: %s", err))

Prevention

When it happens

Trigger: putMD5 (an OTS PutRow on the MD5/lock table) errored after the OSS object was written — OTS endpoint unreachable, table missing, or credentials lacking ots:PutRow.

Common situations: OTS table deleted or renamed mid-run; RAM permissions for OTS narrower than OSS; OTS regional outage; the MD5 row primary key conflicts with an existing row from a different state path.

Related errors


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

Appendix: source

Thrown at internal/backend/remote-state/oss/client.go:129

		options = append(options, oss.ACL(oss.ACLType(c.acl)))
	}
	options = append(options, oss.ContentType("application/json"))
	if c.serverSideEncryption {
		options = append(options, oss.ServerSideEncryption("AES256"))
	}
	options = append(options, oss.ContentLength(int64(len(data))))

	if body != nil {
		if err := bucket.PutObject(c.stateFile, body, options...); err != nil {
			return diags.Append(fmt.Errorf("failed to upload state %s: %#v", c.stateFile, err))
		}
	}

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

func (c *RemoteClient) Delete() tfdiags.Diagnostics {
	var diags tfdiags.Diagnostics
	bucket, err := c.ossClient.Bucket(c.bucketName)
	if err != nil {
		return diags.Append(fmt.Errorf("error getting bucket %s: %#v", c.bucketName, err))
	}

	log.Printf("[DEBUG] Deleting remote state from OSS: %#v", c.stateFile)

	if err := bucket.DeleteObject(c.stateFile); err != nil {
		return diags.Append(fmt.Errorf("error deleting state %s: %#v", c.stateFile, err))
	}

	if err := c.deleteMD5(); err != nil {

View on GitHub (pinned to d32a084675)