Rust notify on Windows: Do Not Drain Forever Inside the Watcher Callback
File watchers are tempting places to do work. A file changed, so you read new data, parse it, update state, and maybe keep looping until there is nothing left.
On Windows, that can make a notify-based watcher appear frozen. The callback path should be treated as an event delivery path, not a place for unbounded parsing.
The anti-pattern
This shape is risky:
let mut watcher = notify::recommended_watcher(move |event| {
if event.is_err() {
return;
}
loop {
match read_next_complete_chunk() {
Some(chunk) => process_chunk(chunk),
None => break,
}
}
})?;
If process_chunk is slow or read_next_complete_chunk keeps finding buffered data, the callback stays busy. While it is busy, new file-system events wait behind it.
The watcher did not stop receiving events. Your callback stopped returning.
Process bounded work per callback
Prefer one chunk per event:
let mut watcher = notify::recommended_watcher(move |event| {
if event.is_err() {
return;
}
if let Some(chunk) = read_next_complete_chunk() {
process_chunk(chunk);
}
})?;
This returns control to the watcher quickly. If more writes arrive, the next event gives the application another chance to process the next chunk.
Move heavy work to another thread or async task
For heavier parsing, send work to a queue:
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
std::thread::spawn(move || {
while let Ok(chunk) = rx.recv() {
process_chunk(chunk);
}
});
let mut watcher = notify::recommended_watcher(move |event| {
if event.is_err() {
return;
}
if let Some(chunk) = read_next_complete_chunk() {
let _ = tx.send(chunk);
}
})?;
The callback remains small. The worker can parse, batch, debounce, or update application state without blocking event delivery.
Trade-off: trailing data may wait
Processing one chunk per callback has a trade-off. If a final write produces no later event, some buffered data may wait until another event arrives.
You can handle that with a timer or a separate flush path:
// Pseudocode:
// - watcher callback sends one chunk
// - periodic timer checks for a final complete chunk
// - shutdown drains remaining complete data once
The important part is that the callback itself remains bounded.
Debouncing is related but not identical
Debouncing coalesces bursts of events. Bounded callbacks prevent the event delivery path from being occupied indefinitely.
You often need both:
notify callback
-> enqueue small signal quickly
-> debounce or batch outside callback
-> parse/process in worker
Do not implement debounce by sleeping inside the watcher callback. Sleeping there blocks event delivery too.
Verification checklist
Review watcher callbacks for these risks:
loopwithout a strict iteration limit- parsing large files inside the callback
- blocking network or database calls
- sleeps or long debounce delays inside the callback
- UI state updates that can trigger heavy recomputation
- locks held while reading or parsing files
Then verify under load:
- Write many small updates quickly.
- Write one large update.
- Confirm events continue after the large update starts.
- Confirm shutdown drains or preserves remaining complete data.
References
Summary
A file watcher callback should be fast and bounded. On Windows especially, do not drain and parse forever inside the notify callback. Process one chunk, enqueue work, return, and let a worker or timer handle the heavy path.