{"record":{"id":"43a15f301dd8daf1","repo":"t8y2/dbx","slug":"etcd-cas-conflict-43a15f","errorCode":"ETCD_CAS_CONFLICT","errorMessage":"ETCD_CAS_CONFLICT: key changed after it was loaded","messagePattern":"ETCD_CAS_CONFLICT: key changed after it was loaded","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agents/drivers/etcd2-go/kv.go","lineNumber":232,"sourceCode":"\n\tform := url.Values{}\n\tform.Set(\"value\", value)\n\tif hasTtl {\n\t\tform.Set(\"ttl\", strconv.FormatInt(*ttlValue, 10))\n\t}\n\tif expectedModRevision != nil {\n\t\tform.Set(\"prevIndex\", strconv.FormatInt(*expectedModRevision, 10))\n\t}\n\tif expectedCreateRevision != nil && *expectedCreateRevision == 0 {\n\t\tform.Set(\"prevExist\", \"false\")\n\t}\n\n\tctx, cancel := s.beginOperation()\n\tdefer s.endOperation(cancel)\n\tbody, _, err := client.do(ctx, http.MethodPut, v2KeyPath(key), form.Encode(), nil)\n\tif err != nil {\n\t\tif isCompareFailed(err) {\n\t\t\treturn nil, errors.New(\"ETCD_CAS_CONFLICT: key changed after it was loaded\")\n\t\t}\n\t\treturn nil, err\n\t}\n\tvar parsed v2KeysResponse\n\tif err := json.Unmarshal(body, &parsed); err != nil {\n\t\treturn nil, err\n\t}\n\tvar revision int64\n\tif parsed.Node != nil {\n\t\trevision = parsed.Node.ModifiedIndex\n\t}\n\treturn map[string]any{\"revision\": longString(revision)}, nil\n}\n\nfunc (s *etcd2Session) delete(params map[string]json.RawMessage) (any, error) {\n\tclient, err := s.activeClient()\n\tif err != nil {\n\t\treturn nil, err","sourceCodeStart":214,"sourceCodeEnd":250,"githubUrl":"https://github.com/t8y2/dbx/blob/c0390bff16418b651f4728520d99adf8ce48829a/agents/drivers/etcd2-go/kv.go#L214-L250","documentation":"The etcd2 driver performs compare-and-swap writes by sending prevIndex/prevExist parameters to the etcd v2 HTTP API. When etcd rejects the write because the key's modifiedIndex no longer matches what the caller loaded, the client maps the 412 Precondition Failed response to this error. It signals a lost optimistic-concurrency race: another writer modified the key between your read and your put.","triggerScenarios":"Calling put with expectedModRevision (or prevExist=false for create-only) while a concurrent client modified or created the key, causing etcd to return a 412 compare-failed response.","commonSituations":"Two workers read the same config key and both try to write it back; a leader-election loop where multiple instances race to update a lock value; a stale client holding an old revision after a network retry; automated scripts and manual etcdctl edits hitting the same key.","solutions":["Re-read the key to get its current modRevision, apply your change to the fresh value, and retry the put with the new expectedModRevision.","If the write is create-only (expectedCreateRevision=0 / prevExist=false), first GET the key to check whether it already exists, or accept the conflict and treat the key as existing.","Reduce the read-modify-write window or serialize writes for that key through a single writer (queue/leader).","Check your expectedModRevision source — you may be caching an old revision; always take it from the most recent get result."],"exampleFix":"// before\nresult, err := agent.Put(ctx, key, newValue, WithExpectedModRevision(staleRev))\n// after\nrev, err := refreshRevision(agent, key) // re-GET the key, recompute value from fresh state\nif err != nil { return err }\nresult, err := agent.Put(ctx, key, recompute(newValue), WithExpectedModRevision(rev))","handlingStrategy":"retry","validationCode":"node, err := agent.Get(ctx, key)\nif err != nil { return err }\nif node.ModRevision == 0 { return errors.New(\"key has no revision yet\") }\n// use node.ModRevision as expectedModRevision immediately after this read","typeGuard":"func isCasConflict(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"ETCD_CAS_CONFLICT\")\n}","tryCatchPattern":"err := agent.Put(ctx, key, val, WithExpectedModRevision(rev))\nif isCasConflict(err) {\n    node, gerr := agent.Get(ctx, key)\n    if gerr != nil { return gerr }\n    rev = node.ModRevision\n    // recompute value from fresh state and retry, with backoff/attempt cap\n}","preventionTips":["Always source expectedModRevision from the immediately preceding GET, never from cache","Keep read-modify-write windows short; re-read before every conditional write","Retry CAS conflicts with exponential backoff and a bounded attempt count","Serialize writes to hot keys through a single writer or lock"],"tags":["etcd","cas-conflict","concurrency","optimistic-locking"],"backgroundTag":"cas-write-conflict","analyzedSha":"c0390bff16418b651f4728520d99adf8ce48829a","analyzedAt":"2026-09-05T23:05:10.900Z","contentChangedAt":"2026-09-05T23:05:10.900Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}