hashicorp/terraform · error

failed to store state MD5: %s

Error message

failed to store state MD5: %s

What it means

In RemoteClient.Put() (client.go:126-129), putMD5 failed. putMD5 writes the state's MD5 digest into TableStore so future Get() calls can detect staleness. The comment at line 127-128 explains the hard fail: with OSS updated but OTS digest stale, the next Get will inevitably mismatch (error 348), so Put aborts rather than ship an inconsistent state.

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 c9def3e214)

Solutions

  1. Confirm the OTS table still exists and credentials have tablestore:PutRow.
  2. Raise OTS reserved throughput or switch to on-demand capacity if throttling.
  3. Re-run apply once OTS recovers; if the OSS object was written but digest missing, manually write the Digest row to match (per error 348 guidance).
  4. If locking/digest is not needed, drop ots_endpoint/tablestore_table so putMD5 is a no-op (returns nil at line 256).

Example fix

// before: OTS required but throttled
backend "oss" {
  bucket = "tf-state"
  ots_endpoint = "https://...ots.aliyuncs.com"
  tablestore_table = "terraform_lock"
}

// after: drop OTS (no locking/digest) to unblock
backend "oss" {
  bucket = "tf-state"
}
Defensive patterns

Strategy: retry

Validate before calling

// Probe OTS PutRow capability with a throwaway conditional write before the real apply.
func otsWritable(c *tablestore.TableStoreClient, table string) error {
    // attempt + immediate delete on a synthetic key
    return nil // implement a round-trip probe if OTS flakiness is common in your env
}

Try / catch

// Retry on OTS throttling; if persistent, drop OTS config (no-op putMD5) or repair digest.
if strings.Contains(err.Error(), "failed to store state MD5") {
    backoffRetry()
}

Prevention

When it happens

Trigger: putMD5()'s otsClient.PutRow (client.go:286) returns a non-nil error (note putMD5 currently swallows the error via log at line 291, but the wrapping at line 129 reflects the original). Triggered by OTS outage, throttling, missing tablestore:PutRow permission, or table deleted between init and apply.

Common situations: OTS instance throttled during a burst of applies; RAM policy revoked PutRow mid-session; OTS table dropped; region-wide OTS degradation.

Related errors


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