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

  1. Remove WithLimit from the watch call
  2. If you need to page historical events, issue a Get with WithRev/WithLimit per page and start the watch from the last returned revision
  3. Keep pagination logic in the read path only; watches have no page size
  4. 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

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


AI-assisted analysis of etcd-io/etcd@f744d457f4 (2026-08-15). Data as JSON: /api/errors/3d5fd984a1f44ab8. Report an issue: GitHub.