tirth8205/code-review-graph · critical · RuntimeError
watch observer stopped: dead thread(s) {names}
Error message
watch observer stopped: dead thread(s) {names} What it means
The watch loop detects that one or more watchdog observer threads have died, and exits with RuntimeError instead of continuing with a silently-stale graph. The message names the dead thread(s) so the daemon supervisor can restart the watcher.
Source
Thrown at code_review_graph/incremental.py:2462
# nothing else would ever catch the stale side.
handler.dispatch(DirDeletedEvent(path))
handler.dispatch(DirCreatedEvent(path))
if dead:
names = ", ".join(dead)
supervisor.report_health(
observer_alive=False,
last_event_at=handler.last_event_at,
events_seen=handler.events_seen,
dead_threads=tuple(dead),
force=True,
)
logger.error(
"Filesystem watcher thread(s) died (%s); %s would stop updating "
"silently, so this watcher is exiting for the daemon to restart it",
names,
repo_root,
)
raise RuntimeError(f"watch observer stopped: dead thread(s) {names}")
supervisor.report_health(
observer_alive=True,
last_event_at=handler.last_event_at,
events_seen=handler.events_seen,
)
supervisor.clear_health()
except KeyboardInterrupt:
supervisor.clear_health()
_run_time_boxed(observer.stop, "observer stop")
finally:
restore_sigterm()
_run_time_boxed(observer.stop, "observer stop")
observer.join(timeout=_WATCH_STOP_TIMEOUT)
handler.stop()
logger.info("Watch stopped.")
def start_watch_thread(View on GitHub (pinned to b58668751a)
Solutions
- Raise the inotify limit: 'sudo sysctl fs.inotify.max_user_watches=524288' (persist in /etc/sysctl.conf) — the most common cause of dead observer threads on Linux.
- Check the logged error just above the raise; the watcher logs why the thread(s) died before exiting.
- Ensure only one watcher instance runs per repository to avoid resource exhaustion.
- Restart the daemon; the watcher exits precisely so the supervisor can bring it back with fresh threads.
Example fix
# before watchdog start /path/to/repo # observer threads keep dying # after sudo sysctl fs.inotify.max_user_watches=524288 sudo sysctl -p watchdog start /path/to/repo
Defensive patterns
Strategy: retry
Validate before calling
import os
if not os.path.exists('/proc/sys/fs/inotify/max_user_watches') is False:
pass
# Linux: check watch budget before starting
import subprocess
limit = int(subprocess.run(['sysctl','-n','fs.inotify.max_user_watches'],capture_output=True,text=True).stdout.strip() or 0)
if limit and limit < len(list(repo_root.rglob('*'))):
raise SystemExit('raise fs.inotify.max_user_watches before watching this repo') Try / catch
try:
updater.watch(repo_root)
except RuntimeError as exc:
if str(exc).startswith('watch observer stopped'):
restart_watcher_with_backoff(updater, repo_root) Prevention
- Raise fs.inotify.max_user_watches on Linux for large repos.
- Run the watcher under a supervisor that restarts it with backoff.
- Avoid multiple concurrent watchers over the same tree.
When it happens
Trigger: Running watch() (or the daemon 'start'/'run' flow) when the underlying watchdog Observer thread crashes or exits — commonly caused by inotify watch limits being exhausted on Linux, or the observer being stopped externally.
Common situations: Linux systems where fs.inotify.max_user_watches is exceeded on large repos; multiple watchers on the same tree; observer threads killed by OOM or errors inside watchdog.
Related errors
AI-assisted analysis of tirth8205/code-review-graph@b58668751a (2026-08-28).
Data as JSON: /api/errors/9fc6af266dfa85e3.
Report an issue: GitHub.