t8y2/dbx · error
ETCD_CAS_CONFLICT
ETCD_CAS_CONFLICT
Error message
ETCD_CAS_CONFLICT: key changed after it was loaded
What it means
Optimistic-concurrency (compare-and-swap) failure thrown by put at kv.go:294. When put is called with expectedModRevision and/or expectedCreateRevision, the driver wraps the write in an etcd transaction whose If condition asserts the key's current revision(s) still match. If another writer changed the key between your read and your write, response.Succeeded is false and this error is returned; the write is NOT applied.
Source
Thrown at agents/drivers/etcd-go/kv.go:294
leaseID = clientv3.LeaseID(*leaseValue)
}
revision, err := func() (int64, error) {
if expectedModRevision != nil || expectedCreateRevision != nil {
var comparisons []clientv3.Cmp
if expectedModRevision != nil {
comparisons = append(comparisons, clientv3.Compare(clientv3.ModRevision(key), "=", *expectedModRevision))
}
if expectedCreateRevision != nil {
comparisons = append(comparisons, clientv3.Compare(clientv3.CreateRevision(key), "=", *expectedCreateRevision))
}
txn := client.Txn(ctx).If(comparisons...).Then(clientv3.OpPut(key, value, clientv3.WithLease(leaseID)))
response, err := txn.Commit()
if err != nil {
return 0, err
}
if !response.Succeeded {
return 0, errors.New("ETCD_CAS_CONFLICT: key changed after it was loaded")
}
return response.Header.Revision, nil
}
response, err := client.Put(ctx, key, value, clientv3.WithLease(leaseID))
if err != nil {
return 0, err
}
return response.Header.Revision, nil
}()
if err != nil && grantedLeaseID != 0 {
_, _ = client.Lease.Revoke(context.Background(), grantedLeaseID)
}
if err != nil {
return nil, err
}
return map[string]any{"revision": longString(revision)}, nil
}
View on GitHub (pinned to c0390bff16)
Solutions
- Re-read the key (get returns metadata with the current mod revision) and retry the put with the fresh expectedModRevision.
- Use an exponential-backoff retry loop around read-then-put, since CAS contention is expected under concurrency.
- If strict CAS is unnecessary, drop expectedModRevision/expectedCreateRevision and do a plain put.
- Reduce contention by sharding keys or serializing writes through a single owner (lease-based leader).
Example fix
// before
put(session, key, value, { expectedModRevision: staleRev })
// after
for (let i = 0; i < 5; i++) {
const cur = get(session, key, { metadataOnly: true });
try {
return put(session, key, value, { expectedModRevision: cur.metadata.modRevision });
} catch (e) {
if (!String(e).includes('ETCD_CAS_CONFLICT')) throw e;
}
} Defensive patterns
Strategy: retry
Validate before calling
const cur = get(session, key, { metadataOnly: true });
const expected = cur.found ? cur.metadata.modRevision : 0; // capture fresh CAS baseline immediately before put Type guard
function isCasConflict(e) { return String(e && e.message || e).includes('ETCD_CAS_CONFLICT'); } Try / catch
for (let attempt = 0; attempt < 5; attempt++) {
try { return put(session, key, value, { expectedModRevision: currentModRev(key) }); }
catch (e) { if (!isCasConflict(e) || attempt === 4) throw e; }
} Prevention
- Always re-read the revision immediately before a CAS put; never reuse stale expectedModRevision across retries.
- Use jittered exponential backoff on CAS retries.
- Make writes idempotent so a conflict after partial success is harmless.
- Serialize writers to hot keys via leader election or a lock key.
When it happens
Trigger: Calling put with expectedModRevision (or expectedCreateRevision) whose value no longer matches the key's live MVCC revision because a concurrent put/delete on the same key landed first.
Common situations: Multiple instances/workers doing read-modify-write on a shared config key; a background reconciler updating the key between your GET and your PUT; retries replaying an old expectedModRevision after a first successful write.
Related errors
- ETCD_V2_CAS_UNSUPPORTED
- user is required
- Not connected
- Client certificate and key must be provided together
- No etcd endpoint configured
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/649a77ce8e217840.
Report an issue: GitHub.