etcd-io/etcd · error
unexpected lease in watch
Error message
unexpected lease in watch
What it means
OpWatch panics with 'unexpected lease in watch' when WithLease is applied to a watch. Attaching a lease to a key is a write-time concept (the key expires with the lease); a watch cannot carry a lease, so OpWatch rejects the option at construction time. The panic occurs synchronously in OpWatch, before any gRPC stream is opened.
Source
Thrown at client/v3/op.go:343
}
return ret
}
// OpTxn returns "txn" operation based on given transaction conditions.
func OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {
clonedCmps := make([]Cmp, len(cmps))
for i := range cmps {
clonedCmps[i] = cmps[i].Clone()
}
return Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}
}
func OpWatch(key string, opts ...OpOption) Op {
ret := Op{t: tRange, key: []byte(key)}
ret.applyOpts(opts)
switch {
case ret.leaseID != 0:
panic("unexpected lease in watch")
case ret.limit != 0:
panic("unexpected limit in watch")
case ret.sort != nil:
panic("unexpected sort in watch")
case ret.serializable:
panic("unexpected serializable in watch")
case ret.countOnly:
panic("unexpected countOnly in watch")
case ret.minModRev != 0, ret.maxModRev != 0:
panic("unexpected mod revision filter in watch")
case ret.minCreateRev != 0, ret.maxCreateRev != 0:
panic("unexpected create revision filter in watch")
}
return ret
}
func (op *Op) applyOpts(opts []OpOption) {
for _, opt := range opts {View on GitHub (pinned to f744d457f4)
Solutions
- Remove WithLease from the watch call; leases belong on OpPut via WithLease(leaseID)
- Create/attach the lease on the put side: Grant the lease, then OpPut(key, val, WithLease(id)), then Watch(key) with watch-only options
- Split option builders per operation type so lease options cannot leak into watches
- Add a small test that constructs every watch your code creates; this panic family will catch regressions
Example fix
// before
op := clientv3.OpWatch("k", clientv3.WithLease(sessLease))
// after
leaseResp, _ := cli.Grant(ctx, 30)
_ = clientv3.OpPut("k", "v", clientv3.WithLease(leaseResp.ID))
wop := clientv3.OpWatch("k") Defensive patterns
Strategy: validation
Validate before calling
// Lease belongs on the write; the watch is separate.
l, _ := cli.Grant(ctx, 30)
_ = clientv3.OpPut("k", "v", clientv3.WithLease(l.ID))
wop := clientv3.OpWatch("k", clientv3.WithPrefix()) // no lease Prevention
- Attach WithLease only at the put site (or via concurrency session)
- Never reuse lease-put option slices for watches
- Keep session/lease management in one module
When it happens
Trigger: Calling clientv3.OpWatch(key, clientv3.WithLease(id)) or passing WithLease in the options of an op later converted to a watch request. Common when a helper that attaches a lease to written keys is reused for the watch that observes those keys.
Common situations: Session/lease-based code (e.g. services using WithLease for ephemeral keys via concurrency package) that also watches those keys and shares option assembly; copy-paste between put and watch setup; feature flags that swap a put for a watch without pruning options.
Related errors
- unexpected limit in watch
- unexpected sort in watch
- unexpected serializable in watch
- unexpected countOnly in watch
- unexpected mod revision filter in watch
AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15).
Data as JSON: /api/errors/1f65a19dbc620326.
Report an issue: GitHub.