t8y2/dbx · error
ETCD_WATCH_NOT_FOUND
ETCD_WATCH_NOT_FOUND
Error message
ETCD_WATCH_NOT_FOUND: watch does not exist
What it means
watchPoll looks up the watch state by 'watchId' in the session's watch registry. If no watch with that ID exists, polling cannot proceed and this error is thrown. A watch may be absent because it was never started, already terminated (terminal watches are auto-removed in watchPoll), or belongs to a different session.
Source
Thrown at agents/drivers/etcd-go/watch.go:357
row["metadata"] = metadataMap(item)
}
return row
}
func eventType(event *clientv3.Event) string {
if event.Type == mvccpb.DELETE {
return "delete"
}
return "put"
}
func (s *etcdSession) watchPoll(params map[string]json.RawMessage) (any, error) {
watchID := stringOrDefault(params, "watchId", "")
s.watchesMu.Lock()
state := s.watches[watchID]
s.watchesMu.Unlock()
if state == nil {
return nil, errors.New("ETCD_WATCH_NOT_FOUND: watch does not exist")
}
result := state.poll()
if _, terminal := result["terminal"]; terminal {
if removed := s.removeWatch(watchID); removed != nil {
removed.close()
}
}
return result, nil
}
func (s *etcdSession) watchStop(params map[string]json.RawMessage) (any, error) {
if state := s.removeWatch(stringOrDefault(params, "watchId", "")); state != nil {
state.close()
}
return map[string]bool{"stopped": true}, nil
}
View on GitHub (pinned to c0390bff16)
Solutions
- Use the watchId exactly as returned by watchStart
- Treat this error as terminal: stop polling and start a new watch via watchStart
- On session restart, discard old watch IDs and re-create watches
- Guard against polling the same watch from two consumers
Example fix
// before
result := poll(oldWatchID) // removed after terminal event
// after
if isWatchNotFound(err) { state = startWatch(key, prefix) } Defensive patterns
Strategy: try-catch
Validate before calling
func (s *Session) hasWatch(id string) bool {
s.watchesMu.Lock(); defer s.watchesMu.Unlock()
return s.watches[id] != nil
} Try / catch
res, err := agent.handle(ctx, "watchPoll", map[string]json.RawMessage{"watchId": idJSON})
if err != nil {
if strings.Contains(err.Error(), "ETCD_WATCH_NOT_FOUND") {
id = restartWatch(key, prefix) // recreate and resume
return
}
return err
} Prevention
- Persist watch IDs only per-session; discard after restart
- Stop polling immediately after a terminal result (the watch is auto-removed)
- Route all polls for one watch through a single consumer/goroutine
- Log watch removals so stale IDs are detectable
When it happens
Trigger: Calling watchPoll with a watchId that was never returned by watchStart, reusing a watchId after the watch reached a terminal result (it is removed on first terminal poll), or polling a watch created in a different session.
Common situations: Clients caching watch IDs after restarts; double-consuming a terminal watch event; race where two pollers race and one removes the watch first; typo'd or truncated ID.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- ETCD_WATCH_SCOPE_INVALID
- ETCD_WATCH_NOT_FOUND
- ETCD_WATCH_LIMIT
- query session not found
- ETCD_HISTORY_TIMEOUT
AI-assisted analysis of t8y2/dbx@c0390bff16 (2026-09-05).
Data as JSON: /api/errors/7a3383f8ee2f98b0.
Report an issue: GitHub.