etcd-io/etcd · error

unexpected serializable in watch

Error message

unexpected serializable in watch

What it means

OpWatch panics with 'unexpected serializable in watch' when WithSerializable is applied to a watch. Serializable reads may be served stale from followers, which contradicts the guarantee watches need (events from the linearizable store); the option is therefore rejected by OpWatch's validation. The panic fires at construction time, before any stream is established.

Source

Thrown at client/v3/op.go:349

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

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

View on GitHub (pinned to f744d457f4)

Solutions

  1. Remove WithSerializable from the watch call
  2. If stale reads are acceptable, apply WithSerializable only to OpGet calls in your read helper
  3. Audit central option factories for blanket-applied flags; scope them per operation type
  4. Cover watch construction in unit tests so the panic surfaces during CI

Example fix

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

// after
wop := clientv3.OpWatch("k")
getOp := clientv3.OpGet("k", clientv3.WithSerializable()) // stale reads OK here
Defensive patterns

Strategy: validation

Validate before calling

// Serializable reads only:
getOp := clientv3.OpGet("k", clientv3.WithSerializable())
wop := clientv3.OpWatch("k")

Prevention

When it happens

Trigger: Calling clientv3.OpWatch(key, clientv3.WithSerializable()); sharing an options slice built for stale-tolerant reads (common in read-heavy caches) with watch setup; helper functions that stamp WithSerializable on every operation to 'reduce load'.

Common situations: Performance tuning passes that blanket-apply WithSerializable across all client calls; multi-region deployments relying on follower reads where the same option list is reused for subscriptions; copy-paste between a Get and a Watch block.

Related errors


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