t8y2/dbx · error
ETCD_CAS_CONFLICT
ETCD_CAS_CONFLICT
Error message
ETCD_CAS_CONFLICT: key changed after it was loaded
What it means
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.
Source
Thrown at agents/drivers/etcd2-go/kv.go:232
form := url.Values{}
form.Set("value", value)
if hasTtl {
form.Set("ttl", strconv.FormatInt(*ttlValue, 10))
}
if expectedModRevision != nil {
form.Set("prevIndex", strconv.FormatInt(*expectedModRevision, 10))
}
if expectedCreateRevision != nil && *expectedCreateRevision == 0 {
form.Set("prevExist", "false")
}
ctx, cancel := s.beginOperation()
defer s.endOperation(cancel)
body, _, err := client.do(ctx, http.MethodPut, v2KeyPath(key), form.Encode(), nil)
if err != nil {
if isCompareFailed(err) {
return nil, errors.New("ETCD_CAS_CONFLICT: key changed after it was loaded")
}
return nil, err
}
var parsed v2KeysResponse
if err := json.Unmarshal(body, &parsed); err != nil {
return nil, err
}
var revision int64
if parsed.Node != nil {
revision = parsed.Node.ModifiedIndex
}
return map[string]any{"revision": longString(revision)}, nil
}
func (s *etcd2Session) delete(params map[string]json.RawMessage) (any, error) {
client, err := s.activeClient()
if err != nil {
return nil, errView on GitHub (pinned to c0390bff16)
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.
Example fix
// before
result, err := agent.Put(ctx, key, newValue, WithExpectedModRevision(staleRev))
// after
rev, err := refreshRevision(agent, key) // re-GET the key, recompute value from fresh state
if err != nil { return err }
result, err := agent.Put(ctx, key, recompute(newValue), WithExpectedModRevision(rev)) Defensive patterns
Strategy: retry
Validate before calling
node, err := agent.Get(ctx, key)
if err != nil { return err }
if node.ModRevision == 0 { return errors.New("key has no revision yet") }
// use node.ModRevision as expectedModRevision immediately after this read Type guard
func isCasConflict(err error) bool {
return err != nil && strings.Contains(err.Error(), "ETCD_CAS_CONFLICT")
} Try / catch
err := agent.Put(ctx, key, val, WithExpectedModRevision(rev))
if isCasConflict(err) {
node, gerr := agent.Get(ctx, key)
if gerr != nil { return gerr }
rev = node.ModRevision
// recompute value from fresh state and retry, with backoff/attempt cap
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- JDBC Session was quarantined while waiting for a connection
- Interrupted while waiting for a JDBC workload lease
- Interrupted while waiting for the JDBC physical connection b
- Agent session already exists: {sessionId}
- No document matched _id <id>. It may have been deleted or it
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/43a15f301dd8daf1.
Report an issue: GitHub.