{"record":{"id":"f7371eaf6f87765f","repo":"hashicorp/terraform","slug":"consul-cas-failed-with-transaction-errors-w","errorCode":null,"errorMessage":"consul CAS failed with transaction errors: %w","messagePattern":"consul CAS failed with transaction errors: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/consul/client.go","lineNumber":246,"sourceCode":"\t\t\t&consulapi.KVTxnOp{\n\t\t\t\tVerb:  verb,\n\t\t\t\tKey:   c.Path,\n\t\t\t\tValue: payload,\n\t\t\t\tIndex: c.modifyIndex,\n\t\t\t},\n\t\t}\n\n\t\tok, resp, _, err := kv.Txn(txOps, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t// transaction was rolled back\n\t\tif !ok {\n\t\t\tvar resultErr error\n\t\t\tfor _, respError := range resp.Errors {\n\t\t\t\tresultErr = errors.Join(resultErr, errors.New(respError.What))\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"consul CAS failed with transaction errors: %w\", resultErr)\n\t\t}\n\n\t\tif len(resp.Results) != 1 {\n\t\t\t// this probably shouldn't happen\n\t\t\treturn fmt.Errorf(\"expected on 1 response value, got: %d\", len(resp.Results))\n\t\t}\n\n\t\tc.modifyIndex = resp.Results[0].ModifyIndex\n\n\t\t// We remove all the old chunks\n\t\tcleanupOldChunks()\n\n\t\treturn nil\n\t}\n\n\tif err = store(payload); err == nil {\n\t\t// The payload was small enough to be stored\n\t\treturn diags","sourceCodeStart":228,"sourceCodeEnd":264,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/consul/client.go#L228-L264","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: terraform { backend \"consul\" { path = \"tf/proj\" } } // no lock default\n// after: confirm only one writer; re-fetch then write\nterraform init -reconfigure\nterraform apply","handlingStrategy":"retry","validationCode":"// Before Put, ensure modifyIndex is fresh by re-reading first\nif _, diags := client.Get(); diags.HasErrors() {\n    return diags\n}\n// now client.modifyIndex reflects the latest Consul value, reducing CAS rollback","typeGuard":null,"tryCatchPattern":"// Retry Put a few times on CAS rollback, re-fetching modifyIndex each attempt\nfor attempt := 0; attempt < 3; attempt++ {\n    if _, diags := client.Get(); diags.HasErrors() { return diags }\n    if d := client.Put(data); !d.HasErrors() { return nil }\n    time.Sleep(backoff)\n}","preventionTips":["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."],"tags":["consul","remote-state","cas","transaction","concurrency","lock"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}