Updated on
FileSystemWatcher watches one directory and raises an event when a file inside it is created, deleted, renamed or written to. The operating system detects each change, and the class is the .NET wrapper that passes it on to us.
Filter picks the files we hear about and NotifyFilter picks the kinds of change. Nothing fires until EnableRaisingEvents is set to true.
Two behaviors surprise people once the watcher runs: one save can produce three events, and a busy directory can produce fewer events than it should.
What Is FileSystemWatcher in C#?
FileSystemWatcher is a class in the System.IO namespace that watches one directory and raises an event whenever something inside it changes. It does not poll the disk: the operating system reports each change, and the class hands it to us.
Three settings decide what we hear about. Path is the directory to watch. Filter narrows the watch to a file pattern such as *.txt. NotifyFilter decides which kinds of change count, from a file’s name through to its security settings.
The class raises five events: Created, Deleted, Changed, Renamed and Error. None of them fires until we set EnableRaisingEvents to true.
The watcher works on a local disk or a network share, and reaches into subdirectories once we opt in with IncludeSubdirectories. It never tells us who made the change or what the file now contains, only that a path we care about appeared, vanished, was renamed or was written to.
Let’s inspect how we can set up the FileSystemWatcher in our project:
public class TextFileManager
{
private readonly FileSystemWatcher _fileSystemWatcher;
public TextFileManager(string rootDirectory)
{
_fileSystemWatcher = new FileSystemWatcher(rootDirectory);
}
}
We can initialize FileSystemWatcher simply by passing the path we want to monitor. Throughout this article, we will use our TextFileManager class to configure and work with FileSystemWatcher instance. Of course, you can always download our source code to see the full implementation.
The sample triggers its events with ordinary calls such as File.WriteAllLines(), File.Move() and File.Delete(), and our article on the File and Directory classes covers them in detail.
How Do We Filter FileSystemWatcher Notifications?
Two properties do the filtering, and they answer different questions. Filter decides which files we watch. NotifyFilter decides which changes to those files reach us.
Filter takes a single wildcard pattern and defaults to *, which watches every file. Setting it to *.txt limits the watcher to text files. For several patterns at once there is Filters, a collection, so one instance can watch *.txt and *.log without us creating two watchers.
NotifyFilter takes a bitwise OR combination of NotifyFilters values, and it is already set for us: the default is LastWrite, FileName and DirectoryName combined. Adding Size or Attributes widens the watch, and leaving a value out narrows it.
A narrow watch is also the safer one. Every notification the watcher accepts takes room in a fixed-size internal buffer, so each extra NotifyFilters value and each extra subdirectory makes an overflow, and with it a missed event, more likely.
Filter
Filter is FileSystemWatcher property that enables us to monitor specific files by specifying a file pattern. The default Filter value is *, which monitors all files, and *.* or an empty string does the same thing.
Let’s configure the type of files we want to monitor:
private void ConfigureFilters()
{
_fileSystemWatcher.Filter = "*.txt";
}
Now _fileSystemWatcher monitors only text file changes.
Setting Filter puts its pattern into the Filters collection, so adding *.log to Filters afterwards makes the same watcher report changes to log files as well as text files.
NotifyFilter
NotifyFilter is another property of FileSystemWatcher that enables us to specify and monitor specific changes in the file system. We can configure NotifyFilter to listen to multiple types of changes using the bitwise OR operator("|"). By default, the value of NotifyFilter is a bitwise OR combination of LastWrite, DirectoryName, and FileName, but we can expand that:
private void ConfigureFilters()
{
...
_fileSystemWatcher.NotifyFilter = NotifyFilters.Attributes
| NotifyFilters.CreationTime
| NotifyFilters.DirectoryName
| NotifyFilters.FileName
| NotifyFilters.LastAccess
| NotifyFilters.LastWrite
| NotifyFilters.Security
| NotifyFilters.Size;
}
By combining NotifyFilters values we can expand the range of changes we monitor. The enumeration has eight values, and the last column shows which three are already in the default:
NotifyFilters value | Change it reports | In the default? |
|---|---|---|
FileName | The name of the file | Yes |
DirectoryName | The name of the directory | Yes |
LastWrite | The date the file or folder last had anything written to it | Yes |
Attributes | The attributes of the file or folder | No |
Size | The size of the file or folder | No |
LastAccess | The date the file or folder was last opened | No |
CreationTime | The time the file or folder was created | No |
Security | The security settings of the file or folder | No |
We can also monitor changes inside subdirectories by using the IncludeSubdirectories property:
private void ConfigureFilters()
{
...
_fileSystemWatcher.IncludeSubdirectories = true;
...
}
Which Events Does FileSystemWatcher Raise?
FileSystemWatcher consists of 5 different event types we can use. Namely: Created, Deleted, Changed, Renamed, and Error.
All five handlers below come from the sample’s TextFileManager class, and none of them fires until EnableRaisingEvents is set, which the last snippet in this section does.
Created
We can configure it to trigger an event on the creation of a file or a directory:
private void ConfigureEvents()
{
_fileSystemWatcher.Created += HandleCreated;
}
private void HandleCreated(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Create: {e.FullPath}");
}
Deleted
We can configure it to trigger an event on the deletion of a file or directory:
private void ConfigureEvents()
{
_fileSystemWatcher.Deleted += HandleDeleted;
...
}
private void HandleDeleted(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Delete: {e.FullPath}");
}
Changed
We can configure it to trigger an event when there is a change to the size, system attributes, the last update, the last access time, or permission of a file or directory:
private void ConfigureEvents()
{
_fileSystemWatcher.Changed += HandleChanged;
...
}
private void HandleChanged(object sender, FileSystemEventArgs e)
{
Console.WriteLine($"Change: {Enum.GetName(e.ChangeType)} {e.FullPath}");
}
The event handlers of Created, Deleted and Changed have similar method signatures.
Renamed
We can use it to trigger an event when there is a file name or directory name change:
private void ConfigureEvents()
{
_fileSystemWatcher.Renamed += HandleRenamed;
...
}
private void HandleRenamed(object sender, RenamedEventArgs e)
{
Console.WriteLine($"Rename: {e.OldName} => {e.Name}");
}
Renamed event handlers have a different method signature from Created, Deleted, and Changed event handlers. The second argument of type RenamedEventArgs contains the old name and new name properties of the affected file or directory.
Error
For different reasons FileSystemWatcher could reach a state where it can no longer monitor file system changes in the specified path. To trigger an event in this scenario we use the Error event handler:
private void ConfigureEvents()
{
_fileSystemWatcher.Error += HandleError;
...
}
private void HandleError(object sender, ErrorEventArgs e)
{
Console.WriteLine($"Error: {e.GetException().Message}");
}
Finally, to enable _fileSystemWatcher to raise events, we have to set EnableRaisingEvents property to true:
private void ConfigureEvents()
{
...
_fileSystemWatcher.EnableRaisingEvents = true;
}
FileSystemWatcher events are raised to corresponding file system changes inside the path being monitored and in accordance with the filters. Here are all five events together, with the argument each handler receives:
| Event | Handler argument | Raised when |
|---|---|---|
Created | FileSystemEventArgs | A matching file or directory appears in the watched path |
Deleted | FileSystemEventArgs | A matching file or directory is removed |
Changed | FileSystemEventArgs | A matching item's contents, size, attributes, timestamps or security settings change |
Renamed | RenamedEventArgs (adds OldName and OldFullPath) | A matching file or directory is renamed |
Error | ErrorEventArgs (call GetException()) | The watcher loses track of changes, typically on a buffer overflow, or stops watching because the watched directory itself disappeared |
Common file system operations, such as a move or a copy, might trigger more than one event.
Why Does FileSystemWatcher Raise Multiple Events for One Change?
Because one action in the file system is rarely one operation on disk. Saving a file in an editor can write the bytes, update the timestamp and touch the attributes, and each of those is a separate notification.
Microsoft documents the behavior. Moving a file between directories can raise several Changed events together with Created and Deleted, because a move is made of several simpler operations. Other software on the machine adds events too, antivirus scanners in particular, and the watcher reports that activity like any other.
Handlers therefore have to tolerate repetition. One way is to remember the last path and timestamp we acted on and ignore a repeat inside a short window. Another is to queue every event and let one consumer decide what is worth doing.
Created also fires as soon as the file appears, not when whoever is writing it has finished, so reading it immediately can hit a locked or half-written file.

The internal buffer is the only place in this path where notifications can be lost.
The FileSystemWatcher documentation says it plainly: “Common file system operations might raise more than one event.” Its example is a file moved from one directory to another, which it says can raise “several OnChanged and some OnCreated and OnDeleted events”.
Before a handler reads a file it was just told about, it can check whether a file is still in use and try again later if the writer still holds it.
For the queue, we can hand each event to a queue with .NET Channels: the handler writes the event to the channel and returns at once, and a single consumer reads from it at its own pace.
The path in FileSystemEventArgs does not say whether it names a file or a directory. A handler that needs to know has to check whether a given path is a file or a directory itself, which only works while the path still exists.
How Do We Stop FileSystemWatcher From Missing Events?
By keeping the internal buffer from overflowing. The operating system stores change notifications in a buffer the watcher creates. InternalBufferSize defaults to 8192 bytes, and each notification costs up to 16 bytes plus the file name. When the buffer fills, notifications are lost and the watcher raises Error.
Four moves keep it from filling. Narrow NotifyFilter to the changes we actually handle. Leave IncludeSubdirectories off unless we need it. Prefer short file names in the watched directory, because the name is the part of each notification that varies in size. Keep handlers short, and hand slow work to another thread.
Raise InternalBufferSize only after those four. Microsoft documents 4 KB as the floor and 64 KB as the ceiling, and the property silently rounds anything below 4096 bytes back up to 4096. The buffer also comes from non-paged memory, which cannot be swapped to disk, so a larger buffer is a real cost on the machine.
To avoid buffer overflow:
- Minimize the types of changes we want to monitor using
NotifyFilterandIncludeSubdirectories - Monitor only the files that are of interest by specifying the file pattern. Avoid monitoring files with long names, because each notification costs up to 16 bytes plus its file name
- Keep the event handler code to the possible minimum
- If there is still not enough room, increase the buffer size by setting the
InternalBufferSizeproperty - Handle the
Errorevent. AnInternalBufferOverflowExceptionmeans notifications were lost while the watcher kept running, but ifEnableRaisingEventshas turnedfalse, as it does when the watched directory is deleted, re-create the watcher
Conclusion
FileSystemWatcher needs three properties to report file system events, Path, Filter and NotifyFilter, plus EnableRaisingEvents to switch it on.
The rest of the work is in the handlers. They should assume that every change can arrive more than once and that a newly created file is not ready to read yet, and the watch should stay narrow so the buffer does not overflow and drop notifications.
A watcher raises events only until it is disposed, so in a long-running application we can run the watcher inside a background task that owns it for as long as the host runs.
Tested with .NET 10.0.10.
