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 diagsView on GitHub (pinned to c9def3e214)
Solutions
- Ensure locking is enabled for the consul backend so only one writer is active at a time.
- Wait for the in-progress run to finish, then re-run `terraform apply`/`terraform init` so Get fetches a fresh modifyIndex.
- If locking was bypassed, enable it and re-run; do not force a blind overwrite.
- 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
- Enable locking on the consul backend so concurrent writers are serialized.
- Avoid running two terraform processes against the same state path simultaneously.
- Re-fetch state (Get) immediately before a Put so the cached modifyIndex is current.
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
- expected on 1 response value, got: %d
- error unmarshaling lock info: %s
- state %q already locked
- failed to lock state in Consul: %s
- The remote state does not match the expected hash
AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07).
Data as JSON: /api/errors/f7371eaf6f87765f.
Report an issue: GitHub.