Skip to content

Resilience

When the inotify watch limit is reached (ENOSPC), go-filewatcher does not crash. Instead:

  • The error is logged via the error handler
  • The WatchErrors counter is incremented
  • Walking continues to add remaining directories
  • The watcher starts in degraded mode
watcher, _ := filewatcher.New(paths)
stats := watcher.Stats()
fmt.Printf("Watch errors: %d\n", stats.WatchErrors)

go-filewatcher auto-detects the inotify watch budget from /proc/sys/fs/inotify/max_user_watches on Linux. Override with WithMaxWatches:

watcher, _ := filewatcher.New(
[]string{"./large-monorepo"},
filewatcher.WithMaxWatches(524288), // Override budget
)
stats := watcher.Stats()
fmt.Printf("watching %d/%d paths (%.1f%% budget)\n",
stats.WatchCount, stats.WatchBudgetCap,
stats.WatchBudgetUsed*100)

When the budget is exhausted, directories are skipped silently and counted in Stats.WatchErrors.

WithSelfHeal automatically retries failed watch registrations at the given interval:

watcher, _ := filewatcher.New(
[]string{"./src"},
filewatcher.WithSelfHeal(30 * time.Second), // Retry every 30s
)

This handles transient failures like temporary ENOSPC or mount points that become available later.

For environments where OS-native inotify events don’t work (NFS, FUSE, Docker volumes), enable polling mode:

watcher, _ := filewatcher.New(
[]string{"/mnt/nfs/share"},
filewatcher.WithPolling(true),
filewatcher.WithPollInterval(2 * time.Second),
)

Polling supplements fsnotify events. The poll loop maintains a filesystem snapshot and detects new, modified, and removed files at each interval.

Enabled by default (WithGitignore(true)). Directories matching .gitignore patterns are skipped during the walk — they’re never added to inotify:

// Default: enabled
watcher, _ := filewatcher.New(paths)
// Explicitly disable
watcher, _ := filewatcher.New(paths,
filewatcher.WithGitignore(false),
)

Exclude entire subtrees by absolute path during the walk:

watcher, _ := filewatcher.New(
[]string{"/home/me/projects"},
filewatcher.WithExcludePaths(
"/home/me/projects/forks", // Skip forks entirely
"/home/me/projects/archived", // Skip archived projects
),
)

Uses prefix matching: excluding /home/me/forks skips all subdirectories too.

Follow symbolic links during directory walking:

watcher, _ := filewatcher.New(paths,
filewatcher.WithFollowSymlinks(true), // Default: false
)

Directories are collected during the walk and added in batches of 1000, with runtime.Gosched() between batches. This reduces startup latency for large directory trees and yields to event processing.

On shared or multi-tenant machines, the auto-detected inotify limit includes watches held by other processes. Use a safety fraction to leave headroom:

watcher, _ := filewatcher.New(
[]string{"./large-monorepo"},
filewatcher.WithMaxWatchesSafetyFraction(0.75), // Use 75% of system limit
)
stats := watcher.Stats()
fmt.Printf("system limit: %d, effective cap: %d\n",
stats.WatchLimit, stats.WatchBudgetCap)

The fraction only applies to auto-detected limits — explicit WithMaxWatches(n) values are used as-is. Default is 1.0 (no reduction).

By default, a slow consumer blocks the watch loop when the event channel is full. This preserves all events but can cause kernel-level event loss if the consumer is consistently slow.

Switch to drop-on-full mode to prioritize watcher liveness:

watcher, _ := filewatcher.New(
[]string{"./src"},
filewatcher.WithBuffer(64),
filewatcher.WithEventChannelMode(filewatcher.EventChannelDropOnFull),
)
stats := watcher.Stats()
if stats.EventsDroppedByBackpressure > 0 {
log.Warn("events dropped due to slow consumer",
"dropped", stats.EventsDroppedByBackpressure)
}

The error channel uses the same buffer size as the event channel by default. Under high error load (e.g., many ENOSPC errors during a large tree walk), errors can be silently dropped. Decouple the buffer sizes:

watcher, _ := filewatcher.New(
[]string{"./huge-tree"},
filewatcher.WithBuffer(64), // event channel
filewatcher.WithErrorBufferSize(1024), // error channel (larger)
)
stats := watcher.Stats()
if stats.ErrorsDropped > 0 {
log.Warn("errors dropped", "count", stats.ErrorsDropped)
}

The watcher classifies errors as transient or permanent. Self-heal abandons permanent errors immediately instead of retrying forever:

  • Permanent: os.ErrPermission, os.ErrNotExist, syscall.ENOTDIR
  • Transient: syscall.ENOSPC (may resolve when resources free up)

This prevents self-heal from wasting cycles retrying paths that will never succeed.