etcd-io/etcd · error

unexpected mod revision filter in watch

Error message

unexpected mod revision filter in watch

What it means

OpWatch panics with 'unexpected mod revision filter in watch' when WithMinModRev or WithMaxModRev is applied to a watch. Revision-window filters restrict range results to keys modified in a revision range; watches are configured with a start revision (WithRev), not filters, so OpWatch rejects these options. The panic occurs synchronously in OpWatch.

Source

Thrown at client/v3/op.go:353

	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.
func WithLease(leaseID LeaseID) OpOption {
	return func(op *Op) { op.leaseID = leaseID }

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithMinModRev/WithMaxModRev from the watch call
  2. To resume a watch from a point in history, use WithRev(startRevision) on the watch and optionally WithPrefix for ranges
  3. Keep the revision-window filters on the catch-up Get only
  4. Ensure option assembly for watch ops is a separate, minimal builder

Example fix

// before
op := clientv3.OpWatch("k", clientv3.WithMinModRev(500))

// after
catchUp := clientv3.OpGet("k", clientv3.WithMinModRev(500))
wop := clientv3.OpWatch("k", clientv3.WithRev(lastSeenRev+1))
Defensive patterns

Strategy: validation

Validate before calling

// Resume watches with WithRev, not revision filters:
wop := clientv3.OpWatch("k", clientv3.WithRev(lastRev+1))

Prevention

When it happens

Trigger: Calling clientv3.OpWatch(key, clientv3.WithMinModRev(n)) or WithMaxModRev(n); converting a filtered historical read into a watch while keeping the filter options; generic sync helpers applying the same filters to reads and watches.

Common situations: Incremental-sync or backup tools that read a mod-revision window then watch for live changes; code sharing one options slice between the catch-up read and the subscription; version upgrades where option lists got merged during refactor.

Related errors


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