etcd-io/etcd · error
unexpected lease in delete
Error message
unexpected lease in delete
What it means
After applying options, OpDelete verifies that no read/get-only option leaked into a delete operation. WithLease sets op.leaseID, which is only meaningful for Put (attach a lease to a key); a delete has no lease to attach, so ret.leaseID != 0 panics with "unexpected lease in delete". The check exists because applyOpts silently stores the option and the mismatch would otherwise be dropped or mis-sent to the server.
Source
Thrown at client/v3/op.go:277
if IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) {
panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one")
}
ret := Op{t: tRange, key: []byte(key)}
ret.applyOpts(opts)
return ret
}
// OpDelete returns "delete" operation based on given key and operation options.
func OpDelete(key string, opts ...OpOption) Op {
// WithPrefix and WithFromKey are not supported together
if IsOptsWithPrefix(opts) && IsOptsWithFromKey(opts) {
panic("`WithPrefix` and `WithFromKey` cannot be set at the same time, choose one")
}
ret := Op{t: tDeleteRange, key: []byte(key)}
ret.applyOpts(opts)
switch {
case ret.leaseID != 0:
panic("unexpected lease in delete")
case ret.limit != 0:
panic("unexpected limit in delete")
case ret.rev != 0:
panic("unexpected revision in delete")
case ret.sort != nil:
panic("unexpected sort in delete")
case ret.serializable:
panic("unexpected serializable in delete")
case ret.countOnly:
panic("unexpected countOnly in delete")
case ret.minModRev != 0, ret.maxModRev != 0:
panic("unexpected mod revision filter in delete")
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
panic("unexpected create revision filter in delete")
case ret.filterDelete, ret.filterPut:
panic("unexpected filter in delete")
case ret.createdNotify:
panic("unexpected createdNotify in delete")View on GitHub (pinned to f744d457f4)
Solutions
- Remove WithLease from the Delete call — leases are attached on Put; deletion revokes nothing about the lease.
- If the intent is 'delete the key when the lease expires', that is automatic: just attach the lease at Put time and let it expire.
- If the intent is to revoke the lease, call cli.Revoke(ctx, id) instead of Delete with WithLease.
- Split option lists per operation kind in shared helpers (putOpts vs delOpts).
Example fix
// before _, err := cli.Delete(ctx, "/session/1", clientv3.WithLease(sessLease.ID())) // panics // after _, err := cli.Delete(ctx, "/session/1") // to free the lease itself: cli.Revoke(ctx, sessLease.ID())
Defensive patterns
Strategy: validation
Prevention
- Keep Put options (WithLease, WithPrevKV, WithIgnoreLease, WithRequireLeader) and Delete options in separate variables.
- Leases attach at Put; expiry removes the key automatically — never pass WithLease to Delete.
- To free a lease, call Revoke, not Delete.
When it happens
Trigger: clientv3.OpDelete("/k", clientv3.WithLease(leaseID)) or cli.Delete(ctx, "/k", clientv3.WithLease(id)); sharing one []OpOption built for a Put with a Delete call; helper APIs that take 'op options' and forward them to every operation type.
Common situations: A generic write(key, val, opts...) helper where val=="" routes to Delete but options still include WithLease; retry wrappers that cache options across operation kinds; team conventions that bundle lease options with key lifecycle code that also deletes on expiry handling.
Related errors
- unexpected limit in delete
- unexpected revision in delete
- unexpected sort in delete
- unexpected serializable in delete
- unexpected countOnly in delete
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/eda42da2345e52ab.
Report an issue: GitHub.