t8y2/dbx · error
Cannot preserve lease: key does not exist or has no lease
Error message
Cannot preserve lease: key does not exist or has no lease
What it means
Thrown by putPreservingLease (kv.go:320), reached when put is called with preserveLease:true. Before writing, the function GETs the key to learn its current lease; if the key does not exist or exists with no attached lease (Lease <= 0), there is nothing to preserve and this error is returned. Lease preservation only makes sense for keys already owned by an active lease.
Source
Thrown at agents/drivers/etcd-go/kv.go:320
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
}
func putPreservingLease(client *clientv3.Client, ctx context.Context, key, value string) (int64, error) {
for attempt := 0; attempt < preserveLeaseMaxAttempts; attempt++ {
existing, err := client.Get(ctx, key)
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()View on GitHub (pinned to c0390bff16)
Solutions
- Ensure the key exists with an active lease before using preserveLease — create it once with { ttl: N } first.
- Fall back to a plain put (no preserveLease) when the key is absent, e.g. get first and branch.
- Check the key's metadata lease via get before every preserveLease put.
- If the lease expired, re-grant it with ttl instead of trying to preserve it.
- Add a refresh loop (keepalive) for the lease if keys must outlive the TTL.
Example fix
// before
put(session, key, value, { preserveLease: true }) // key missing -> error
// after
const cur = get(session, key);
if (cur.found && cur.metadata.lease) put(session, key, value, { preserveLease: true });
else put(session, key, value, { ttl: 60 }); Defensive patterns
Strategy: validation
Validate before calling
const cur = get(session, key);
if (!cur.found || !cur.metadata.lease) {
throw new Error('preserveLease requires an existing key with an active lease');
} Type guard
function canPreserveLease(keyState) { return keyState.found === true && keyState.metadata != null && keyState.metadata.lease > 0; } Try / catch
try { return put(session, key, value, { preserveLease: true }); }
catch (e) {
if (String(e).includes('key does not exist or has no lease'))
return put(session, key, value, { ttl: 60 }); // bootstrap the lease
throw e;
} Prevention
- Create keys with { ttl: N } at least once before any preserveLease writes.
- Refresh/renew leases (keepalive) so keys don't silently expire.
- get() with metadata and check lease > 0 before preserveLease in shared helpers.
- Don't enable preserveLease as a blanket default for all writes.
When it happens
Trigger: put(..., { preserveLease: true }) on a key that was never created with a lease/ttl, or on a key whose lease already expired (etcd auto-deleted the key), or on a not-yet-existing key.
Common situations: Using preserveLease as a blanket default for all writes including initial creation; lease TTL expiring between writes so the key vanishes; key previously written without ttl and code assumes it has one; key created by another tool without a lease.
Related errors
- lease, ttl, and preserveLease cannot be specified together
- ttl must be a positive integer
- Cannot preserve lease: key changed concurrently; retry the s
- 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/b02ebbe897b043ae.
Report an issue: GitHub.