Skip to main content

How to Create a Real-Time Log Viewer (Tail -f) in Batch Script

On Linux systems, the tail -f command is a staple for developers and administrators, allowing them to watch a log file update in real-time as new data is appended. Windows Command Prompt doesn't have a native tail command. However, you can achieve this behavior using PowerShell's built-in Get-Content -Wait parameter, or build an approximation in pure Batch for legacy environments. A real-time log viewer is essential for debugging live applications, monitoring server traffic, or watching a backup process as it runs.

This guide will explain how to build a tail -f equivalent for Windows.

PowerShell's Get-Content cmdlet has a -Wait parameter that perfectly replicates tail -f. It holds the file open and outputs new lines as they are appended, efficiently, without polling or re-reading the entire file. This is the recommended approach for any system running PowerShell 3.0 or later (Windows 8+).

@echo off
setlocal

set "LogPath=%~1"

:: If no argument provided, show usage
if "%LogPath%"=="" (
echo Usage: %~nx0 ^<logfile^>
echo.
echo Example: %~nx0 C:\Logs\server.log
endlocal
exit /b 1
)

:: Verify the file exists
if not exist "%LogPath%" (
echo [ERROR] Log file not found: %LogPath% >&2
endlocal
exit /b 1
)

echo [MONITOR] Watching: %LogPath%
echo [MONITOR] Showing last 20 lines, then waiting for new content.
echo [MONITOR] Press Ctrl+C to stop.
echo ------------------------------------------

:: -Tail 20 = Show the last 20 lines immediately for context
:: -Wait = Keep watching for new lines appended to the file
powershell -NoProfile -Command "Get-Content -Path '%LogPath%' -Tail 20 -Wait"

endlocal
exit /b 0

Why this is the best approach:

  • No polling: Get-Content -Wait uses file system notifications internally, so it consumes minimal CPU while waiting for changes.
  • No re-reading: Unlike Batch loop approaches, it does not re-read the entire file on each check. It tracks its position and outputs only new content.
  • Handles file locking gracefully: It opens the file with shared read access, so it works even while another application is actively writing to the log.
  • Immediate output: New lines appear within milliseconds of being written, providing true real-time monitoring.

Making it reusable:

Save this script as tail.bat and place it in a directory on your PATH. You can then type tail C:\Logs\server.log from any terminal to start monitoring.

Method 2: Filtered Tail (Watching for Specific Patterns)

Often you don't want to see every log line, only errors, warnings, or lines matching a specific keyword. PowerShell makes this easy by piping Get-Content -Wait through Where-Object.

@echo off
setlocal

set "LogPath=%~1"
set "Filter=%~2"

if "%LogPath%"=="" (
echo Usage: %~nx0 ^<logfile^> [filter]
echo.
echo Examples:
echo %~nx0 C:\Logs\app.log Watch all lines
echo %~nx0 C:\Logs\app.log ERROR Watch only ERROR lines
echo %~nx0 C:\Logs\app.log "WARN|ERROR" Watch WARN and ERROR lines
endlocal
exit /b 1
)

if not exist "%LogPath%" (
echo [ERROR] Log file not found: %LogPath% >&2
endlocal
exit /b 1
)

if "%Filter%"=="" (
echo [MONITOR] Watching all lines in: %LogPath%
powershell -NoProfile -Command "Get-Content -Path '%LogPath%' -Tail 20 -Wait"
) else (
echo [MONITOR] Watching lines matching "%Filter%" in: %LogPath%
powershell -NoProfile -Command ^
"Get-Content -Path '%LogPath%' -Tail 100 -Wait |" ^
"Where-Object { $_ -match '%Filter%' }"
)

endlocal
exit /b 0

Usage examples:

tail.bat C:\Logs\app.log Watch all lines
tail.bat C:\Logs\app.log ERROR Watch only lines containing "ERROR"
tail.bat C:\Logs\app.log "WARN|ERROR" Watch lines containing WARN or ERROR
tail.bat C:\Logs\app.log "timeout|refused" Watch for connection issues

Why -Tail 100 for filtered mode:

When filtering, a -Tail 20 might not contain any matching lines, giving the impression that nothing is happening. Using a larger initial buffer (-Tail 100) increases the chance of showing recent matches immediately. Lines that don't match the filter are silently discarded by Where-Object.

Method 3: Pure Batch Tail (Legacy Fallback)

If PowerShell is unavailable (extremely rare, but possible on embedded or locked-down systems), you can approximate a tail with a Batch loop. This approach re-reads the file periodically using more +N to skip previously seen lines.

@echo off
setlocal EnableDelayedExpansion

set "LogPath=%~1"
set "Interval=2"

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

if not exist "%LogPath%" (
echo [ERROR] Log file not found: %LogPath% >&2
endlocal
exit /b 1
)

echo [MONITOR] Watching: %LogPath% (pure Batch mode, polling every %Interval%s^)
echo [MONITOR] Press Ctrl+C to stop.
echo ------------------------------------------

:: Count initial lines and show the last 20
set "LineCount=0"
for /f %%n in ('find /c /v "" ^< "%LogPath%"') do set "LineCount=%%n"

:: Show last 20 lines as initial context
set /a "SkipInitial=!LineCount! - 20"
if !SkipInitial! lss 0 set "SkipInitial=0"
more +!SkipInitial! "%LogPath%"

set "LastCount=!LineCount!"

:TailLoop
timeout /t %Interval% >nul

:: Check if the file still exists
if not exist "%LogPath%" (
echo [WARNING] Log file has been deleted or moved. Waiting... >&2
goto :TailLoop
)

:: Get current line count
set "CurrentCount=0"
for /f %%n in ('find /c /v "" ^< "%LogPath%"') do set "CurrentCount=%%n"

:: If new lines were added, display only the new ones
if !CurrentCount! gtr !LastCount! (
more +!LastCount! "%LogPath%"
set "LastCount=!CurrentCount!"
)

:: If file was truncated (rotated), reset
if !CurrentCount! lss !LastCount! (
echo [INFO] Log file was rotated. Resetting position.
set "LastCount=0"
)

goto :TailLoop

Limitations of the pure Batch approach:

AspectPowerShell (Method 1)Pure Batch (Method 3)
Update latencyMilliseconds2+ seconds (polling interval)
CPU usage while idleMinimal (event-driven)Constant (polling loop)
Re-reads file on each checkNoYes (via find /c and more)
Handles file rotationNo (stays attached to original)Yes (detects line count decrease)
Handles large filesYesSlow (more re-reads from the start)
Filtering supportYes (Where-Object)No (would require findstr layer)

When to use the Batch approach:

Use this only when PowerShell is genuinely unavailable. On any modern Windows system (7 or later with updates), Method 1 is superior in every respect.

Log rotation detection:

If the log file is rotated (replaced with a new, smaller file), the line count drops. The script detects this (CurrentCount < LastCount) and resets its position to read from the beginning of the new file. The PowerShell approach does not handle this automatically, Get-Content -Wait stays attached to the original file handle.

How to Avoid Common Errors

Wrong Way: Using cls and type in a Loop

:: BROKEN: re-reads and displays the ENTIRE file every 2 seconds
:Loop
cls
type "%LogFile%"
timeout /t 2 >nul
goto :Loop

This re-reads the entire file on every iteration. For a 10 MB log file, this means reading 10 MB from disk and rendering thousands of lines on screen every 2 seconds, most of which the user has already seen. It also produces constant screen flicker from cls.

Correct Way: Use Get-Content -Wait (Method 1) or the delta-check approach (Method 3) that only outputs new lines.

Problem: File Locking

If the application writing the log opens it with an exclusive lock, any attempt to read the file will fail with "Access Denied" or "The process cannot access the file."

Solution: PowerShell's Get-Content -Wait opens the file with shared read access, which works with most applications. If you still encounter locking issues, the application's logging configuration may need to be changed to use shared write mode.

Problem: Large Initial Buffer Floods the Screen

Starting a tail on a 100,000-line log file without a buffer limit dumps the entire history to the console.

Solution: Always use -Tail N (PowerShell) or skip to the last N lines (Batch) when starting the viewer. 10–20 lines provides sufficient context without flooding the screen.

Problem: Log File Rotation

When a log rotation system renames the active log and creates a new one, Get-Content -Wait stays attached to the old (renamed) file. New entries go to the new file, but the viewer shows nothing.

Solution: The pure Batch approach (Method 3) handles this automatically by detecting a decrease in line count. For the PowerShell approach, you can restart the viewer after detecting the file has been replaced, or use a wrapper that monitors the file's creation time:

:: Simple rotation-aware wrapper
:RestartTail
powershell -NoProfile -Command "Get-Content -Path '%LogPath%' -Tail 20 -Wait"
echo [INFO] Get-Content exited. Restarting in case of file rotation...
timeout /t 2 >nul
if exist "%LogPath%" goto :RestartTail

Best Practices and Rules

1. Limit the Initial Buffer

Always show only the last 10–20 lines when starting the viewer. Use -Tail 20 in PowerShell or calculate the skip offset in Batch. Starting with the full file history is overwhelming and defeats the purpose of a real-time viewer.

2. Accept the Log Path as a Parameter

Make your tail script reusable by accepting the log file path as %~1 rather than hardcoding it. This lets you monitor any log file with a single tool.

3. Prefer PowerShell Over Batch Loops

The Batch polling approach consumes more CPU, has higher latency, and re-reads the file on every check. Use it only when PowerShell is genuinely unavailable. On any modern Windows system, Method 1 is the correct choice.

4. Use Filtering for Busy Logs

On high-traffic servers, a log file may receive hundreds of lines per second. Use Method 2's filtering capability to watch only the lines you care about (errors, specific request IDs, specific usernames) rather than trying to read everything.

5. Set the Console Window Title

When monitoring multiple logs in different terminal windows, set a descriptive title so you can identify each viewer from the taskbar:

title Tail: %LogPath%

Conclusions

Creating a real-time log viewer brings powerful Unix-style monitoring to your Windows environment. PowerShell's Get-Content -Wait provides an efficient, zero-configuration tail -f equivalent that handles file locking, outputs new lines instantly, and supports filtering. For legacy environments, a pure Batch polling loop provides a workable approximation. This real-time visibility is a critical asset for identifying transient errors and understanding the behavior of live systems as events happen.