tailscale/tailscale · error
watcher error: %w
Error message
watcher error: %w
What it means
The fsnotify watcher delivered an error on its Errors channel and the watch loop propagates it via errCh and returns. fsnotify Errors carries low-level inotify failures, most commonly fsnotify.ErrEventOverflow (kernel event queue overflow) or errors from a watch that was removed because the watched directory was deleted or replaced.
Source
Thrown at cmd/containerboot/tailscaled.go:241
}
b, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("error reading configfile: %w", err)
return
}
prevTailscaledCfg = b
// kubelet mounts Secrets to Pods using a series of symlinks, one of
// which is <mount-dir>/..data that Kubernetes recommends consumers to
// use if they need to monitor changes
// https://github.com/kubernetes/kubernetes/blob/v1.28.1/pkg/volume/util/atomic_writer.go#L39-L61
const kubeletMountedCfg = "..data"
toWatch := filepath.Join(tailscaledCfgDir, kubeletMountedCfg)
for {
select {
case <-ctx.Done():
return
case err := <-errChan:
errCh <- fmt.Errorf("watcher error: %w", err)
return
case <-tickChan:
case event := <-eventChan:
if event.Name != toWatch {
continue
}
}
b, err := os.ReadFile(path)
if err != nil {
errCh <- fmt.Errorf("error reading configfile: %w", err)
return
}
// For some proxy types the mounted volume also contains tailscaled state and other files. We
// don't want to reload config unnecessarily on unrelated changes to these files.
if reflect.DeepEqual(b, prevTailscaledCfg) {
continue
}
prevTailscaledCfg = bView on GitHub (pinned to cfe32b8be6)
Solutions
- Reduce unrelated file churn in the config directory (move state files elsewhere)
- Raise kernel limits: sysctl fs.inotify.max_queued_events=65536 and fs.inotify.max_user_watches
- Restart the container to re-establish the watch
- Check whether the directory itself is being replaced by your tooling; update files in place or recreate the watch
Example fix
$ sysctl -w fs.inotify.max_queued_events=65536 $ sysctl -w fs.inotify.max_user_watches=1048576
Defensive patterns
Strategy: retry
Validate before calling
// No pre-check reliably predicts inotify overflow; monitor instead: // fs.file-max, fs.inotify.max_user_watches, fs.inotify.max_queued_events
Try / catch
// In your own watchers, degrade instead of dying on fsnotify errors:
if err := <-watcher.Errors; err != nil {
if errors.Is(err, fsnotify.ErrEventOverflow) {
// rescan state once and keep watching
} else {
// re-add watch / restart watcher
}
} Prevention
- Raise fs.inotify.max_queued_events and max_user_watches on nodes running config watches
- Keep high-churn files (state DBs) out of the watched config directory
- Alert on containerboot restarts so silent watch loss is noticed
- Re-create watchers after directory replacement rather than trusting stale handles
When it happens
Trigger: Very high churn of file events in the watched config directory overflowing fs.inotify.max_queued_events; the watched directory being deleted and recreated (breaking the watch); hitting per-user/system inotify instance or watch limits.
Common situations: Kubelet atomic-writer secret rotations combined with other busy files (tailscaled state) in the same directory; nodes with low fs.inotify.max_queued_events/max_user_watches; processes that rewrite the config dir instead of the files in it.
Related errors
- failed to watch tailscaled config: %w
- failed to add fsnotify watch: %w
- watcher error: %w
- failed to add fsnotify watch: %w
- reading %q: %w
AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15).
Data as JSON: /api/errors/208dd34fc170f14c.
Report an issue: GitHub.