{"record":{"id":"217824f84479d2d2","repo":"etcd-io/etcd","slug":"unexpected-serializable-in-watch","errorCode":null,"errorMessage":"unexpected serializable in watch","messagePattern":"unexpected serializable in watch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":349,"sourceCode":"\tclonedCmps := make([]Cmp, len(cmps))\n\tfor i := range cmps {\n\t\tclonedCmps[i] = cmps[i].Clone()\n\t}\n\treturn Op{t: tTxn, cmps: clonedCmps, thenOps: thenOps, elseOps: elseOps}\n}\n\nfunc OpWatch(key string, opts ...OpOption) Op {\n\tret := Op{t: tRange, key: []byte(key)}\n\tret.applyOpts(opts)\n\tswitch {\n\tcase ret.leaseID != 0:\n\t\tpanic(\"unexpected lease in watch\")\n\tcase ret.limit != 0:\n\t\tpanic(\"unexpected limit in watch\")\n\tcase ret.sort != nil:\n\t\tpanic(\"unexpected sort in watch\")\n\tcase ret.serializable:\n\t\tpanic(\"unexpected serializable in watch\")\n\tcase ret.countOnly:\n\t\tpanic(\"unexpected countOnly in watch\")\n\tcase ret.minModRev != 0, ret.maxModRev != 0:\n\t\tpanic(\"unexpected mod revision filter in watch\")\n\tcase ret.minCreateRev != 0, ret.maxCreateRev != 0:\n\t\tpanic(\"unexpected create revision filter in watch\")\n\t}\n\treturn ret\n}\n\nfunc (op *Op) applyOpts(opts []OpOption) {\n\tfor _, opt := range opts {\n\t\topt(op)\n\t}\n}\n\n// OpOption configures Operations like Get, Put, Delete.\ntype OpOption func(*Op)","sourceCodeStart":331,"sourceCodeEnd":367,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L331-L367","documentation":"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.","triggerScenarios":"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'.","commonSituations":"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.","solutions":["Remove WithSerializable from the watch call","If stale reads are acceptable, apply WithSerializable only to OpGet calls in your read helper","Audit central option factories for blanket-applied flags; scope them per operation type","Cover watch construction in unit tests so the panic surfaces during CI"],"exampleFix":"// before\nop := clientv3.OpWatch(\"k\", clientv3.WithSerializable())\n\n// after\nwop := clientv3.OpWatch(\"k\")\ngetOp := clientv3.OpGet(\"k\", clientv3.WithSerializable()) // stale reads OK here","handlingStrategy":"validation","validationCode":"// Serializable reads only:\ngetOp := clientv3.OpGet(\"k\", clientv3.WithSerializable())\nwop := clientv3.OpWatch(\"k\")","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never add consistency flags to watches","Audit 'reduce load' tunings applied globally","Per-operation-type option builders"],"tags":["etcd","clientv3","panic","api-misuse","watch","serializable"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}