Skip to main content

How to Monitor File Changes in a Directory in Batch Script

Monitoring a directory for file changes, new files, deletions, or modifications, is a core requirement for automated workflows. Whether you're waiting for a printer to drop a PDF, a scanner to finish an image, or a build system to produce an artifact, a Batch script can watch the folder and trigger a reaction. Instead of manually refreshing Windows Explorer, you can build a folder watcher that acts as a trigger for your main automation logic.

This guide will explain how to create a directory monitor using Batch and PowerShell.

For production use, the .NET FileSystemWatcher is the correct tool. It is event-driven, Windows notifies the script the moment a file changes, without any polling or directory re-reading. This uses minimal CPU and provides near-instant detection.

Implementation: Continuous Watch Loop

@echo off
setlocal EnableDelayedExpansion

set "WatchDir=%~1"
set "Filter=*.*"

:: Validate arguments
if "%WatchDir%"=="" (
echo Usage: %~nx0 ^<directory^> [filter]
echo.
echo Examples:
echo %~nx0 C:\Input
echo %~nx0 C:\Input *.pdf
endlocal
exit /b 1
)

if not "%~2"=="" set "Filter=%~2"

if not exist "%WatchDir%\" (
echo [ERROR] Directory not found: %WatchDir% >&2
endlocal
exit /b 1
)

title Watching: %WatchDir% (%Filter%)
echo [MONITOR] Watching: %WatchDir%
echo [MONITOR] Filter: %Filter%
echo [MONITOR] Press Ctrl+C to stop.
echo ------------------------------------------

:WatchLoop
:: Clear previous values to avoid stale data
set "ChangeType="
set "FileName="

for /f "tokens=1-2 delims=|" %%a in ('powershell -NoProfile -Command "$w=New-Object IO.FileSystemWatcher;$w.Path='%WatchDir%';$w.Filter='%Filter%';$w.IncludeSubdirectories=$false;$w.EnableRaisingEvents=$true;$r=$w.WaitForChanged([IO.WatcherChangeTypes]::All,30000);$w.Dispose();if($r.TimedOut){'TIMEOUT|'}else{'{0}|{1}' -f $r.ChangeType,$r.Name}"') do (
set "ChangeType=%%a"
set "FileName=%%b"
)

:: Handle timeout (no changes in 30 seconds) - loop back silently
if "!ChangeType!"=="TIMEOUT" goto :WatchLoop

:: Safety: skip if PS failed to output anything
if not defined ChangeType goto :WatchLoop

echo [%date% %time%] !ChangeType!: !FileName!

:: =============================================
:: React to the change here
:: =============================================
if /i "!ChangeType!"=="Created" call :CheckFile "%WatchDir%" "!FileName!"

goto :WatchLoop

:: =============================================
:: Subroutine to safely process files
:: =============================================
:CheckFile
set "CheckPath=%~1\%~2"
if exist "%CheckPath%" (
echo [ACTION] Processing: %~2
:: call :ProcessFile "%CheckPath%"
)
exit /b

Example of Output:

[MONITOR] Watching: C:\Users\David\Desktop
[MONITOR] Filter: *.*
[MONITOR] Press Ctrl+C to stop.
------------------------------------------
[Thu 04/16/2026 18:29:30.08] Changed: script.bat
[Thu 04/16/2026 18:29:32.36] Changed: script.bat
[Thu 04/16/2026 18:29:38.57] Renamed: hosts_chaanges.log

How FileSystemWatcher works:

  1. The watcher registers with the Windows kernel to receive notifications for the specified directory.
  2. WaitForChanged blocks until an event occurs or the timeout expires, consuming zero CPU while waiting.
  3. When a file is created, modified, deleted, or renamed, Windows delivers the event immediately.
  4. The result includes the ChangeType (Created, Changed, Deleted, Renamed) and the Name of the affected file.

Why a timeout is used:

WaitForChanged with an infinite timeout (-1) would keep the PowerShell process alive indefinitely. Using a 30-second timeout allows the script to periodically return to Batch, check if the directory still exists, and restart the watcher. This also ensures that pressing Ctrl+C actually stops the script rather than being trapped inside an infinite PowerShell wait.

Why $watcher.Dispose():

FileSystemWatcher allocates system resources (file handles, kernel notification buffers). Without Dispose(), each loop iteration would leak a watcher object. Over hours of monitoring, this could exhaust system resources.

Change types returned:

ChangeTypeMeaning
CreatedA new file appeared in the directory
ChangedAn existing file was modified
DeletedA file was removed from the directory
RenamedA file was renamed

Method 2: Snapshot Comparison (Pure Batch Fallback)

If PowerShell is unavailable, you can approximate directory monitoring by periodically comparing directory listings. This approach is significantly less efficient than Method 1 but works on any Windows version.

@echo off
setlocal EnableDelayedExpansion

set "WatchDir=%~1"
set "Interval=5"
set "SnapPrev=%TEMP%\dirwatch_prev_%RANDOM%.txt"
set "SnapCurr=%TEMP%\dirwatch_curr_%RANDOM%.txt"

if "%WatchDir%"=="" (
echo Usage: %~nx0 ^<directory^>
endlocal
exit /b 1
)

if not exist "%WatchDir%\" (
echo [ERROR] Directory not found: %WatchDir% >&2
endlocal
exit /b 1
)

title Watching: %WatchDir% (snapshot mode)
echo [MONITOR] Watching: %WatchDir% (polling every %Interval%s^)
echo [MONITOR] Press Ctrl+C to stop.
echo ------------------------------------------

:: Take initial snapshot
dir /b "%WatchDir%" > "%SnapPrev%" 2>nul

:SnapLoop
timeout /t %Interval% >nul

:: Verify directory still exists (important for network shares)
if not exist "%WatchDir%\" (
echo [WARNING] Directory is inaccessible: %WatchDir% >&2
goto :SnapLoop
)

:: Take current snapshot
dir /b "%WatchDir%" > "%SnapCurr%" 2>nul

:: Compare the two snapshots
fc "%SnapPrev%" "%SnapCurr%" >nul 2>&1
if errorlevel 1 (
echo [%date% %time%] Changes detected in %WatchDir%

:: Show new files (in current but not in previous)
for /f "delims=" %%f in ('findstr /v /x /g:"%SnapPrev%" "%SnapCurr%"') do (
echo [NEW] %%f
)

:: Show deleted files (in previous but not in current)
for /f "delims=" %%f in ('findstr /v /x /g:"%SnapCurr%" "%SnapPrev%"') do (
echo [DEL] %%f
)

:: Update the baseline snapshot
copy /y "%SnapCurr%" "%SnapPrev%" >nul
)

goto :SnapLoop

Example of Output:

[MONITOR] Watching: C:\Users\Davide\Desktop (polling every 5s)
[MONITOR] Press Ctrl+C to stop.
------------------------------------------
[Thu 04/16/2026 18:30:20.27] Changes detected in C:\Users\Davide\Desktop
[NEW] hosts_changes.log
[DEL] hosts_chaanges.log

Limitations of the snapshot approach:

AspectFileSystemWatcher (Method 1)Snapshot (Method 2)
Detection latencyMilliseconds5+ seconds (polling interval)
CPU usage while idleNear zero (event-driven)Constant (polling)
Large directories (10,000+ files)EfficientSlow (dir /b and fc re-read everything)
Detects modificationsYesNo (only name changes; not content changes)
Handles network disconnectsWatcher may stop workingRecovers on next poll

What this approach cannot detect:

dir /b lists only filenames. If a file's content changes but its name stays the same, the snapshots will be identical and the change goes undetected. To detect content modifications, you would need to compare file sizes or timestamps, which significantly increases complexity. Use Method 1 for modification detection.

Why %RANDOM% in temp filenames:

If multiple instances of the script run simultaneously (watching different directories), they need separate temp files. The %RANDOM% suffix prevents collisions.

Method 3: Waiting for a Specific File

A common use case is waiting for a particular file to appear (e.g., a report generated by another process) before proceeding.

@echo off
setlocal

set "WatchDir=%~1"
set "TargetFile=%~2"
set "Timeout=300"
set "Interval=2"

if "%WatchDir%"=="" (
echo Usage: %~nx0 ^<directory^> ^<filename^>
echo.
echo Example: %~nx0 C:\Output report.pdf
endlocal
exit /b 1
)

if "%TargetFile%"=="" (
echo [ERROR] Target filename is required. >&2
endlocal
exit /b 1
)

:: Remove trailing backslash from WatchDir (prevents \\ in path)
if "%WatchDir:~-1%"=="\" set "WatchDir=%WatchDir:~0,-1%"

set "FullPath=%WatchDir%\%TargetFile%"
set /a "Elapsed=0"

echo [INFO] Waiting for: %FullPath%
echo [INFO] Timeout: %Timeout% seconds
echo.

:WaitLoop
if exist "%FullPath%" (
(
echo. >> "%FullPath%"
) 2>nul && (
call :FileReady
exit /b 0
) || (
echo [INFO] File exists but is still being written. Waiting...
)
)

if %Elapsed% geq %Timeout% (
echo [ERROR] Timeout: %TargetFile% did not appear within %Timeout% seconds. >&2
endlocal
exit /b 1
)

timeout /t %Interval% >nul
set /a "Elapsed+=%Interval%"
goto :WaitLoop


::=================================================================
:FileReady
echo [OK] File detected and accessible: %TargetFile%
echo [OK] Appeared after %Elapsed% seconds.

:: Process the file here
:: call :ProcessFile "%FullPath%"

endlocal
exit /b 0
::=================================================================

Example of Output:

[INFO] Waiting for: C:\Users\David\Desktop\scripts.bat
[INFO] Timeout: 300 seconds

[OK] File detected and accessible: scripts.bat
[OK] Appeared after 4 seconds.

Why the file-lock check matters:

When a large file is being copied or generated, the file appears in the directory immediately but is locked by the writing process. Without checking, your script might try to process a partially written file, resulting in corrupt data or read errors. The echo. >> file 2>nul test attempts to open the file for appending. If it succeeds (the && branch), the file is accessible. If it fails (the || branch), the file is still locked.

Important caveat about the lock test:

The echo. >> file approach appends a newline to the file if it succeeds. For files that must not be modified (e.g., executables, archives), use a read-only test instead:

:: Read-only lock test (does not modify the file)
type "%FullPath%" >nul 2>&1 && (
echo [OK] File is accessible.
) || (
echo [INFO] File is still locked.
)

How to Avoid Common Errors

Wrong Way: Polling Large Directories with dir /b

Running dir /b every few seconds on a directory with 10,000+ files causes high CPU usage and disk I/O as the system re-reads the entire directory listing.

Correct Way: Use FileSystemWatcher (Method 1) for large or active directories. It registers for kernel notifications and uses near-zero resources while waiting.

Problem: Processing Partially Written Files

When a 1 GB file is being copied into the watched directory, it appears immediately but isn't fully written for minutes. Processing it before the transfer completes produces corrupt or incomplete results.

Solution: Check whether the file is locked before processing (as shown in Method 3). Alternatively, have the writing process create the file with a temporary name (e.g., .tmp) and rename it when complete. Your watcher then filters for the final extension only.

Problem: Network Share Disconnects

If the watched directory is on a network share that disconnects, FileSystemWatcher may stop receiving events silently, and dir /b will fail.

Solution: Add a directory-existence check at the start of each loop iteration:

if not exist "%WatchDir%\" (
echo [WARNING] Directory inaccessible. Waiting for reconnection... >&2
timeout /t 10 >nul
goto :WatchLoop
)

Both methods in this guide include this check.

Problem: FileSystemWatcher Buffer Overflow

If many changes occur rapidly (e.g., extracting a ZIP with thousands of files), the FileSystemWatcher internal buffer may overflow, causing missed events.

Solution: For high-volume scenarios, increase the buffer size in the PowerShell snippet: $watcher.InternalBufferSize = 65536. The default is 8,192 bytes. Alternatively, use the snapshot approach (Method 2) as a periodic verification that no events were missed.

Best Practices and Rules

1. Filter by Extension

Don't react to every temporary file or system metadata change. Set a filter to watch only the file types you care about:

  • Method 1: $watcher.Filter = "*.pdf"
  • Method 2: dir /b "%WatchDir%\*.pdf"

2. Log Every Detection

Record every detected change with a timestamp for auditing and pipeline performance measurement:

echo [%date% %time%] %ChangeType%: %FileName% >> "%~dp0watch_log.txt"

3. Handle the "Burst" Problem

A single user action (saving a file in Word, extracting an archive) can generate multiple rapid filesystem events (create, modify, modify, rename). Add a brief delay or deduplication logic before processing to avoid reacting to the same file multiple times.

4. Prefer FileSystemWatcher Over Polling

Unless PowerShell is unavailable, always use Method 1. It is faster, uses less CPU, detects more change types (including content modifications), and scales to large directories without performance degradation.

5. Clean Up Temp Files

If using the snapshot approach (Method 2), delete the temporary snapshot files when the script exits. Use a unique filename per instance to prevent collisions when multiple watchers run simultaneously.

Conclusions

Building a directory monitor transforms your Batch scripts into reactive agents. By using FileSystemWatcher for event-driven detection or snapshot comparison for legacy environments, you free yourself from manual checking and ensure your automation triggers the moment a file arrives or changes. This capability is essential for building automated pipelines that process incoming data, respond to system events, and maintain workflow continuity without human intervention.