hashicorp/terraform · error

consul CAS failed with transaction errors: %w

Error message

consul CAS failed with transaction errors: %w

What it means

Raised in RemoteClient.Put()'s store() closure (consul/client.go:246) when a Consul KV transaction is rolled back (kv.Txn returns ok=false). State writes use a Check-And-Set verb keyed on the cached modifyIndex, so a rollback means the key changed since the last read. The rolled-back transaction's per-op errors are joined and wrapped with %w so the caller sees the underlying Consul reason.

Source

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

			&consulapi.KVTxnOp{
				Verb:  verb,
				Key:   c.Path,
				Value: payload,
				Index: c.modifyIndex,
			},
		}

		ok, resp, _, err := kv.Txn(txOps, nil)
		if err != nil {
			return err
		}
		// transaction was rolled back
		if !ok {
			var resultErr error
			for _, respError := range resp.Errors {
				resultErr = errors.Join(resultErr, errors.New(respError.What))
			}
			return fmt.Errorf("consul CAS failed with transaction errors: %w", resultErr)
		}

		if len(resp.Results) != 1 {
			// this probably shouldn't happen
			return fmt.Errorf("expected on 1 response value, got: %d", len(resp.Results))
		}

		c.modifyIndex = resp.Results[0].ModifyIndex

		// We remove all the old chunks
		cleanupOldChunks()

		return nil
	}

	if err = store(payload); err == nil {
		// The payload was small enough to be stored
		return diags

View on GitHub (pinned to c9def3e214)

Solutions

  1. Ensure locking is enabled for the consul backend so only one writer is active at a time.
  2. Wait for the in-progress run to finish, then re-run `terraform apply`/`terraform init` so Get fetches a fresh modifyIndex.
  3. If locking was bypassed, enable it and re-run; do not force a blind overwrite.
  4. Investigate the wrapped error string for the exact Consul refusal reason before retrying.

Example fix

// before: terraform { backend "consul" { path = "tf/proj" } } // no lock default
// after: confirm only one writer; re-fetch then write
terraform init -reconfigure
terraform apply
Defensive patterns

Strategy: retry

Validate before calling

// Before Put, ensure modifyIndex is fresh by re-reading first
if _, diags := client.Get(); diags.HasErrors() {
    return diags
}
// now client.modifyIndex reflects the latest Consul value, reducing CAS rollback

Try / catch

// Retry Put a few times on CAS rollback, re-fetching modifyIndex each attempt
for attempt := 0; attempt < 3; attempt++ {
    if _, diags := client.Get(); diags.HasErrors() { return diags }
    if d := client.Put(data); !d.HasErrors() { return nil }
    time.Sleep(backoff)
}

Prevention

When it happens

Trigger: store() runs kv.Txn with verb KVCAS and the stale modifyIndex; another writer committed first, so Consul rejects the CAS and reports errors in resp.Errors. Also fires if the lock was lost/reacquired and the underlying state moved underneath.

Common situations: Two concurrent `terraform apply` runs against the same Consul state path (locking disabled or a lost lock); a CI job and a manual run racing; the modifyIndex cached from a Get is stale by the time Put runs.

Related errors


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