{"record":{"id":"b182187d66aa1ff5","repo":"t8y2/dbx","slug":"etcd-watch-limit","errorCode":"ETCD_WATCH_LIMIT","errorMessage":"ETCD_WATCH_LIMIT: at most %d watches are allowed per connection","messagePattern":"ETCD_WATCH_LIMIT: at most (.+?) watches are allowed per connection","errorType":"error_code","errorClass":null,"httpStatus":null,"severity":"error","filePath":"agents/drivers/etcd-go/watch.go","lineNumber":219,"sourceCode":"\tdefer s.watchesMu.Unlock()\n\tif _, exists := s.watches[id]; exists {\n\t\treturn false\n\t}\n\ts.watches[id] = state\n\treturn true\n}\n\nfunc (s *etcdSession) removeWatch(id string) *watchState {\n\ts.watchesMu.Lock()\n\tdefer s.watchesMu.Unlock()\n\tstate := s.watches[id]\n\tdelete(s.watches, id)\n\treturn state\n}\n\nfunc (s *etcdSession) watchStart(params map[string]json.RawMessage) (any, error) {\n\tif s.watchCount() >= maxWatches {\n\t\treturn nil, fmt.Errorf(\"ETCD_WATCH_LIMIT: at most %d watches are allowed per connection\", maxWatches)\n\t}\n\tkey, err := keyBytesParam(params)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tscope := stringOrDefault(params, \"scope\", \"key\")\n\tif scope != \"key\" && scope != \"prefix\" {\n\t\treturn nil, errors.New(\"ETCD_WATCH_SCOPE_INVALID: scope must be key or prefix\")\n\t}\n\tclient, err := s.activeClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trequestedRevision := longOrNull(params, \"startRevision\")\n\tvar startedRevision int64\n\tif requestedRevision != nil && *requestedRevision > 0 {\n\t\tstartedRevision = *requestedRevision\n\t} else {","sourceCodeStart":201,"sourceCodeEnd":237,"githubUrl":"https://github.com/t8y2/dbx/blob/c0390bff16418b651f4728520d99adf8ce48829a/agents/drivers/etcd-go/watch.go#L201-L237","documentation":"Each etcdSession allows at most maxWatches concurrently registered watches. watchStart checks s.watchCount() before creating a new watch and returns this coded error (ETCD_WATCH_LIMIT) when the cap is reached, protecting the agent connection from unbounded watch fan-out. Existing watches keep working; only new registrations are refused.","triggerScenarios":"Calling the watch method on a session that already has maxWatches active watches (i.e. s.watchCount() >= maxWatches).","commonSituations":"Watch loops that create a new watch per key instead of reusing one; watches leaked because their ids were never used with watch cancel/stop; long-running services watching an ever-growing key set; low maxWatches relative to workload.","solutions":["Cancel/stop unused watches first to free slots, then retry watchStart","Consolidate to fewer watches by watching key prefixes (scope=prefix) instead of many individual keys","Raise maxWatches if the workload legitimately requires more concurrent watches"],"exampleFix":"// before\nfor _, key := range keys {\n    session.watch(map[string]any{\"key\": key}) // exhausts maxWatches\n}\n\n// after\nsession.watch(map[string]any{\"key\": prefix, \"scope\": \"prefix\"}) // one watch for all keys","handlingStrategy":"fallback","validationCode":"// track active watches client-side and stop before exceeding the cap\nif activeWatchCount >= maxWatches {\n    stopOldestWatch()\n}","typeGuard":"func isWatchLimit(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"ETCD_WATCH_LIMIT\")\n}","tryCatchPattern":"w, err := session.Call(\"watch\", params)\nif isWatchLimit(err) {\n    // cancel stale watches and retry once, or consolidate to a prefix watch\n    cancelUnusedWatches()\n    w, err = session.Call(\"watch\", params)\n}","preventionTips":["Cancel watches when no longer needed","Prefer prefix/scope watches over one watch per key","Track watch ids and reuse them instead of re-registering"],"tags":["go","etcd","watch","resource-limit"],"backgroundTag":"watch-limit-exceeded","analyzedSha":"c0390bff16418b651f4728520d99adf8ce48829a","analyzedAt":"2026-09-05T23:05:10.900Z","contentChangedAt":"2026-09-05T23:05:10.900Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}