Skip to content
Happy Programming Guide
Start learning
C#

FileSystemWatcher Not Working in C#

Events that never fire, fire twice, or stop after a while. The causes: it fell out of scope, EnableRaisingEvents is false, the buffer overflowed, network shares, and how editors save files.

An office workspace with a computer

FileSystemWatcher looks simple and has more ways to silently fail than almost any other class in .NET. Events do not fire because the watcher was garbage-collected or never enabled; they fire twice because of how editors save; they stop because the internal buffer overflowed; and on network shares and Linux they behave differently again. Each has a specific check, and a small pattern at the end handles the lot.

Cause 1: it went out of scope#

CSHARP
void StartWatching()
{
    var watcher = new FileSystemWatcher(@"C:\data");
    watcher.Created += (s, e) => Console.WriteLine(e.FullPath);
    watcher.EnableRaisingEvents = true;
}   // watcher is now unreachable; the garbage collector disposes it, events stop

The watcher is a local variable. When the method returns, nothing references it, and at the next collection it is gone. Keep it in a field, or in a long-lived object, for as long as you need events:

CSHARP
private readonly FileSystemWatcher _watcher;

public Monitor()
{
    _watcher = new FileSystemWatcher(@"C:\data");
    _watcher.Created += OnCreated;
    _watcher.EnableRaisingEvents = true;
}

Cause 2: EnableRaisingEvents is false#

It defaults to false. Nothing happens until you set it to true, after the handlers are attached. Forgetting this line is the second most common cause and produces no error at all.

Cause 3: the filter or the path#

CSHARP
_watcher.Filter = "*.txt";                       // only .txt files; "" or "*.*" for all
_watcher.IncludeSubdirectories = true;           // default false: only the top folder
_watcher.NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite | NotifyFilters.Size;

The Filter is a single pattern; "*.txt|*.csv" does not work. For several patterns use the Filters collection (.NET 5 and later). IncludeSubdirectories is off by default, so files in subfolders are ignored. And NotifyFilter decides which kinds of change raise events: if you are waiting for Changed and the filter does not include LastWrite, you will wait forever.

Check the path is a directory that exists. The constructor throws for a missing path, but a path to a file rather than a folder is a subtler mistake.

Cause 4: events fire twice, or fire before the file is complete#

Saving a file in most editors is not one write. Word, Visual Studio and many others write to a temporary file, delete the original and rename — producing a Created, several Changed, a Deleted and a Renamed event for one save. Even a simple copy raises Created when the file appears and Changed as data arrives, and the file is locked until the copy finishes.

Two techniques:

CSHARP
// 1. Debounce: act only after events for a path go quiet
private readonly ConcurrentDictionary<string, Timer> _pending = new();

private void OnChanged(object sender, FileSystemEventArgs e)
{
    var timer = _pending.GetOrAdd(e.FullPath, _ => new Timer(_ => Process(e.FullPath), null, Timeout.Infinite, Timeout.Infinite));
    timer.Change(500, Timeout.Infinite);      // restart the half-second wait
}
CSHARP
// 2. Wait until the file can be opened exclusively
static bool IsReady(string path)
{
    try
    {
        using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None);
        return true;
    }
    catch (IOException) { return false; }
}

Retry IsReady with a short delay a few times before processing. Together, debouncing and the readiness check turn a burst of noisy events into one action on a complete file.

Cause 5: the buffer overflowed#

Events are queued in a fixed buffer, 8 KB by default. A burst of thousands of changes — unzipping an archive, a build writing output — fills it, the watcher drops events, and raises Error with an InternalBufferOverflowException. If you do not handle Error, you never find out.

CSHARP
_watcher.InternalBufferSize = 64 * 1024;          // up to 64 KB
_watcher.Error += (s, e) => logger.LogWarning(e.GetException(), "Watcher error; rescanning");

Always handle Error. When it fires, the correct response is to rescan the directory, because you have missed something. Keep handlers fast — do the real work on another thread — so the buffer drains quickly.

Cause 6: network shares#

Watching a UNC path or a mapped drive depends on the server sending change notifications, and many do not reliably: NAS devices, some Samba configurations, and cloud-synced folders. Events arrive late, partially, or never. If you must watch a share, combine the watcher with a periodic scan that compares timestamps, and treat the watcher as an optimisation rather than the source of truth.

Cause 7: Linux, macOS and Docker#

On Linux the implementation uses inotify, which has a per-user limit on watched directories. Watching a large tree with subdirectories can exceed it, after which new watches fail:

Terminal
cat /proc/sys/fs/inotify/max_user_watches
sudo sysctl fs.inotify.max_user_watches=524288

Inside Docker, a bind mount from the host does not always forward change events into the container, particularly on Windows and macOS hosts. The DOTNET_USE_POLLING_FILE_WATCHER=1 environment variable switches the hosting file provider used by ASP.NET Core to polling, and for your own watcher the equivalent is a timer-based scan.

A pattern that handles all of it#

  • Hold the watcher in a field; set the filter, subdirectory flag and notify filter explicitly; enable last.
  • Handle Created, Changed, Renamed, Deleted and Error.
  • Debounce per path; check the file is ready before processing; process off the event thread.
  • Rescan on Error, and periodically anyway if the source is a share or a container mount.

Questions people ask#

Why no events for a file created by my own program?

Events are raised for any change, including your own. If you see none, the watcher is disposed, disabled, or filtered. If you see too many, that is your program’s own writes coming back.

Can I watch a single file?

Watch its directory with Filter set to the file name.

Is there an async or IObservable version?

Not built in. Wrapping the events in a Channel or an observable sequence is common and gives a clean way to debounce.

Why does Changed fire when only the timestamp changed?

Because LastWrite is in the notify filter. Remove it if you only care about size or name changes, or compare content hashes before acting.

Where to go next#

Asynchronous programming with C#Read next

Keep reading

C#

Distinct() Not Working on Objects in C#

Distinct removes duplicates by equality, and for classes equality means the same instance. Three fixes: DistinctBy, a record, or IEquatable, and which…

3 min read

C#

C# 14 Extension Members Explained

Extension members let you add properties, static members and operators to types you do not own, not just methods. The new extension…

4 min read

C#

C# Generics

Generic classes, methods and constraints in C#, why the generic List beats ArrayList, and the variance keywords in and out explained plainly.

5 min read

Keep going — pick your next guide

The fastest way to improve is to read one guide, then build the thing it describes. Start with the basics, or jump straight to a project.

Ask a question or share what worked

Your email address will not be published. Required fields are marked *