etcd-io/etcd · error

unexpected countOnly in watch

Error message

unexpected countOnly in watch

What it means

OpWatch panics with 'unexpected countOnly in watch' when WithCountOnly is applied to a watch. CountOnly collapses a range response to just the count of matching keys; a watch returns an event stream, so a count-only mode is rejected. OpWatch validates its option set and panics at construction time.

Source

Thrown at client/v3/op.go:351

		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)
	}
}

// OpOption configures Operations like Get, Put, Delete.
type OpOption func(*Op)

// WithLease attaches a lease ID to a key in 'Put' request.

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithCountOnly from the watch call
  2. If you need both a current count and change notifications, do a count Get plus a plain watch as two separate ops
  3. Build watch options explicitly (WithPrefix, WithPrevKV, ...) rather than reusing read options
  4. Unit-test every op your binary constructs; these panics are cheap to catch in CI

Example fix

// before
op := clientv3.OpWatch("prefix/", clientv3.WithPrefix(), clientv3.WithCountOnly())

// after
cntOp := clientv3.OpGet("prefix/", clientv3.WithPrefix(), clientv3.WithCountOnly())
wop := clientv3.OpWatch("prefix/", clientv3.WithPrefix())
Defensive patterns

Strategy: validation

Validate before calling

// Count and watch are two distinct ops:
cnt := clientv3.OpGet("p/", clientv3.WithPrefix(), clientv3.WithCountOnly())
wop := clientv3.OpWatch("p/", clientv3.WithPrefix())

Prevention

When it happens

Trigger: Calling clientv3.OpWatch(key, clientv3.WithCountOnly()); reusing options from an existence/count check (WithCountOnly + WithPrefix) when subscribing to the same prefix; generic wrappers that forward a user's entire option list into both counts and watches.

Common situations: Health-check or quota code that counts prefix members and then watches the prefix for changes; shared option slices across monitoring paths; refactors moving from poll-count to watch without pruning options.

Related errors


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