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

  1. Re-read the key (get returns metadata with the current mod revision) and retry the put with the fresh expectedModRevision.
  2. Use an exponential-backoff retry loop around read-then-put, since CAS contention is expected under concurrency.
  3. If strict CAS is unnecessary, drop expectedModRevision/expectedCreateRevision and do a plain put.
  4. 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

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


AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05). Data as JSON: /api/errors/649a77ce8e217840. Report an issue: GitHub.