etcd-io/etcd · error
unexpected limit in watch
Error message
unexpected limit in watch
What it means
OpWatch panics with 'unexpected limit in watch' when WithLimit is applied to a watch. Pagination limits are a property of range reads, not streams; a watch delivers events indefinitely, so a limit is rejected at construction time. The guard lives in OpWatch's validation switch and fires before any watch stream starts.
Source
Thrown at client/v3/op.go:345
}
// 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 {
opt(op)
}View on GitHub (pinned to f744d457f4)
Solutions
- Remove WithLimit from the watch call
- If you need to page historical events, issue a Get with WithRev/WithLimit per page and start the watch from the last returned revision
- Keep pagination logic in the read path only; watches have no page size
- Separate option slices for reads vs watches in shared helpers
Example fix
// before
op := clientv3.OpWatch("k", clientv3.WithLimit(100))
// after
wop := clientv3.OpWatch("k")
getOp := clientv3.OpGet("k", clientv3.WithLimit(100)) Defensive patterns
Strategy: validation
Validate before calling
// Paginate with Get + WithRev cursor; watch without limit. resp, _ := cli.Get(ctx, "prefix/", clientv3.WithPrefix(), clientv3.WithLimit(100)) nextRev := resp.Header.Revision wch := cli.Watch(ctx, "prefix/", clientv3.WithPrefix(), clientv3.WithRev(nextRev))
Prevention
- Watches are unpaginated by design; drop WithLimit
- Use revision cursors (WithRev) to resume after paged reads
- Do not share pagination option slices with watch setup
When it happens
Trigger: Calling clientv3.OpWatch(key, clientv3.WithLimit(n)), or Watcher.Watch with an options slice reused from a paginated Get (WithLimit + WithRange/WithPrefix + WithRev). Also arises from generic pagination helpers applied uniformly to every operation.
Common situations: Key-migration or backup tooling that pages through keys with WithLimit and then sets up a watch with the same options; libraries exposing one options struct mapped to every OpOption; refactoring a Get into a historical watch (WithRev) while keeping the limit.
Related errors
- unexpected lease 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/3d5fd984a1f44ab8.
Report an issue: GitHub.