{"record":{"id":"3d5fd984a1f44ab8","repo":"etcd-io/etcd","slug":"unexpected-limit-in-watch","errorCode":null,"errorMessage":"unexpected limit in watch","messagePattern":"unexpected limit in watch","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"client/v3/op.go","lineNumber":345,"sourceCode":"}\n\n// OpTxn returns \"txn\" operation based on given transaction conditions.\nfunc OpTxn(cmps []Cmp, thenOps []Op, elseOps []Op) Op {\n\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}","sourceCodeStart":327,"sourceCodeEnd":363,"githubUrl":"https://github.com/etcd-io/etcd/blob/f744d457f484e9f748a0700b48ef96dcf792df33/client/v3/op.go#L327-L363","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove WithLimit from the watch call","If you need to page historical events, issue a Get with WithRev/WithLimit per page and start the watch from the last returned revision","Keep pagination logic in the read path only; watches have no page size","Separate option slices for reads vs watches in shared helpers"],"exampleFix":"// before\nop := clientv3.OpWatch(\"k\", clientv3.WithLimit(100))\n\n// after\nwop := clientv3.OpWatch(\"k\")\ngetOp := clientv3.OpGet(\"k\", clientv3.WithLimit(100))","handlingStrategy":"validation","validationCode":"// Paginate with Get + WithRev cursor; watch without limit.\nresp, _ := cli.Get(ctx, \"prefix/\", clientv3.WithPrefix(), clientv3.WithLimit(100))\nnextRev := resp.Header.Revision\nwch := cli.Watch(ctx, \"prefix/\", clientv3.WithPrefix(), clientv3.WithRev(nextRev))","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Watches are unpaginated by design; drop WithLimit","Use revision cursors (WithRev) to resume after paged reads","Do not share pagination option slices with watch setup"],"tags":["etcd","clientv3","panic","api-misuse","watch","pagination"],"backgroundTag":null,"analyzedSha":"f744d457f484e9f748a0700b48ef96dcf792df33","analyzedAt":"2026-08-15T09:39:50.079Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}