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

  1. Retry the whole put(preserveLease) call after a small randomized backoff — it is explicitly a 'retry the save' condition.
  2. Reduce write frequency on the key or serialize writers via a leader/lease election.
  3. Re-read the key and use expectedModRevision CAS put yourself with a longer retry budget.
  4. 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

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


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