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.

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.

Appendix: 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)