t8y2/dbx · warning
Cannot preserve lease: key changed concurrently; retry the s
Error message
Cannot preserve lease: key changed concurrently; retry the save
What it means
Exhausted-retry error from putPreservingLease (kv.go:334). With preserveLease:true, the function loops up to preserveLeaseMaxAttempts doing get-then-transactional-put guarded by a ModRevision comparison; if another writer modifies the key on every attempt so no txn ever succeeds, it gives up with this error. It signals sustained write contention on the key during a lease-preserving update.
Source
Thrown at agents/drivers/etcd-go/kv.go:334
if err != nil {
return 0, err
}
if len(existing.Kvs) == 0 || existing.Kvs[0].Lease <= 0 {
return 0, errors.New("Cannot preserve lease: key does not exist or has no lease")
}
current := existing.Kvs[0]
txn := client.Txn(ctx).
If(clientv3.Compare(clientv3.ModRevision(key), "=", current.ModRevision)).
Then(clientv3.OpPut(key, value, clientv3.WithLease(clientv3.LeaseID(current.Lease))))
response, err := txn.Commit()
if err != nil {
return 0, err
}
if response.Succeeded {
return response.Header.Revision, nil
}
}
return 0, errors.New("Cannot preserve lease: key changed concurrently; retry the save")
}
func (s *etcdSession) delete(params map[string]json.RawMessage) (any, error) {
client, err := s.activeClient()
if err != nil {
return nil, err
}
key, err := keyBytesParam(params)
if err != nil {
return nil, err
}
expectedModRevision := longOrNull(params, "expectedModRevision")
ctx, cancel := s.beginOperation()
defer s.endOperation(cancel)
if expectedModRevision != nil {
txn := client.Txn(ctx).
If(clientv3.Compare(clientv3.ModRevision(key), "=", *expectedModRevision)).View on GitHub (pinned to c0390bff16)
Solutions
- Retry the whole put(preserveLease) call after a small randomized backoff — it is explicitly a 'retry the save' condition.
- Reduce write frequency on the key or serialize writers via a leader/lease election.
- Re-read the key and use expectedModRevision CAS put yourself with a longer retry budget.
- Check for buggy writers hammering the same key and fix the loop.
Example fix
// before
put(session, key, value, { preserveLease: true }); // throws under churn
// after
await retry(3, (i) => new Promise(r => setTimeout(r, 50 * 2 ** i))
.then(() => put(session, key, value, { preserveLease: true }))); Defensive patterns
Strategy: retry
Validate before calling
const cur = get(session, key, { metadataOnly: true });
if (!cur.found) throw new Error('cannot preserve lease on missing key'); // pre-check before contended write Type guard
function isLeaseContentionError(e) { return String(e && e.message || e).includes('key changed concurrently'); } Try / catch
async function putPreservingLeaseWithRetry(key, value, attempts = 5) {
for (let i = 0; i < attempts; i++) {
try { return put(session, key, value, { preserveLease: true }); }
catch (e) {
if (!isLeaseContentionError(e) || i === attempts - 1) throw e;
await sleep(50 * 2 ** i + Math.random() * 50);
}
}
} Prevention
- Apply jittered backoff when retrying — the error explicitly invites a retry.
- Identify and throttle other writers hammering the same key.
- Move frequently-updated state to per-writer keys rather than one hot key.
- Use leader election so only one writer updates the key at a time.
When it happens
Trigger: put(..., { preserveLease: true }) on a key being written continuously by other clients during all preserveLeaseMaxAttempts attempts (each attempt's ModRevision guard fails).
Common situations: Hot keys updated by many workers (leader heartbeat, counters); a tight reconciler loop fighting your writer; short preserveLeaseMaxAttempts budget on a very churning key.
Related errors
- lease, ttl, and preserveLease cannot be specified together
- ttl must be a positive integer
- Cannot preserve lease: key does not exist or has no lease
- ETCD_LEASE_LIST_TIMEOUT
- id must be a positive integer or 0
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/46c6ef9f472ff14d.
Report an issue: GitHub.