{"record":{"id":"46c6ef9f472ff14d","repo":"t8y2/dbx","slug":"cannot-preserve-lease-key-changed-concurrently-r","errorCode":null,"errorMessage":"Cannot preserve lease: key changed concurrently; retry the save","messagePattern":"Cannot preserve lease: key changed concurrently; retry the save","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"agents/drivers/etcd-go/kv.go","lineNumber":334,"sourceCode":"\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif len(existing.Kvs) == 0 || existing.Kvs[0].Lease <= 0 {\n\t\t\treturn 0, errors.New(\"Cannot preserve lease: key does not exist or has no lease\")\n\t\t}\n\t\tcurrent := existing.Kvs[0]\n\t\ttxn := client.Txn(ctx).\n\t\t\tIf(clientv3.Compare(clientv3.ModRevision(key), \"=\", current.ModRevision)).\n\t\t\tThen(clientv3.OpPut(key, value, clientv3.WithLease(clientv3.LeaseID(current.Lease))))\n\t\tresponse, err := txn.Commit()\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif response.Succeeded {\n\t\t\treturn response.Header.Revision, nil\n\t\t}\n\t}\n\treturn 0, errors.New(\"Cannot preserve lease: key changed concurrently; retry the save\")\n}\n\nfunc (s *etcdSession) delete(params map[string]json.RawMessage) (any, error) {\n\tclient, err := s.activeClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tkey, err := keyBytesParam(params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\texpectedModRevision := longOrNull(params, \"expectedModRevision\")\n\n\tctx, cancel := s.beginOperation()\n\tdefer s.endOperation(cancel)\n\tif expectedModRevision != nil {\n\t\ttxn := client.Txn(ctx).\n\t\t\tIf(clientv3.Compare(clientv3.ModRevision(key), \"=\", *expectedModRevision)).","sourceCodeStart":316,"sourceCodeEnd":352,"githubUrl":"https://github.com/t8y2/dbx/blob/c0390bff16418b651f4728520d99adf8ce48829a/agents/drivers/etcd-go/kv.go#L316-L352","documentation":"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.","triggerScenarios":"put(..., { preserveLease: true }) on a key being written continuously by other clients during all preserveLeaseMaxAttempts attempts (each attempt's ModRevision guard fails).","commonSituations":"Hot keys updated by many workers (leader heartbeat, counters); a tight reconciler loop fighting your writer; short preserveLeaseMaxAttempts budget on a very churning key.","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."],"exampleFix":"// before\nput(session, key, value, { preserveLease: true }); // throws under churn\n// after\nawait retry(3, (i) => new Promise(r => setTimeout(r, 50 * 2 ** i))\n  .then(() => put(session, key, value, { preserveLease: true })));","handlingStrategy":"retry","validationCode":"const cur = get(session, key, { metadataOnly: true });\nif (!cur.found) throw new Error('cannot preserve lease on missing key'); // pre-check before contended write","typeGuard":"function isLeaseContentionError(e) { return String(e && e.message || e).includes('key changed concurrently'); }","tryCatchPattern":"async function putPreservingLeaseWithRetry(key, value, attempts = 5) {\n  for (let i = 0; i < attempts; i++) {\n    try { return put(session, key, value, { preserveLease: true }); }\n    catch (e) {\n      if (!isLeaseContentionError(e) || i === attempts - 1) throw e;\n      await sleep(50 * 2 ** i + Math.random() * 50);\n    }\n  }\n}","preventionTips":["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."],"tags":["etcd","lease","contention","retry"],"backgroundTag":"concurrent-write-contention","analyzedSha":"c0390bff16418b651f4728520d99adf8ce48829a","analyzedAt":"2026-09-05T23:05:10.900Z","contentChangedAt":"2026-09-05T23:05:10.900Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}