How to Compress Old Log Files Automatically in Batch Script
Text-based log files are highly compressible. A 100 MB log file can often be shrunk to less than 5 MB when zipped. If you are managing a server with limited disk space, leaving months of plain-text logs on the drive wastes storage and makes the log directory unwieldy. By automating the compression of old logs (those older than a configurable number of days), you can maintain years of searchable history while consuming only a fraction of the storage space. Modern Windows 10/11 includes the tar command, making this possible without third-party tools.
This guide will explain how to find and compress old log files using Batch.
Method 1: Compress Individual Log Files by Age
This method uses forfiles to identify log files older than a configurable threshold and compresses each one individually, preserving the original filename in the archive name. The original file is deleted only after successful compression.
@echo off
setlocal EnableDelayedExpansion
set "LogDir=%~1"
set "DaysOld=7"
set "ArchiveDir="
if "%LogDir%"=="" (
echo Usage: %~nx0 ^<log_directory^> [days_old] [archive_directory]
echo.
echo Examples:
echo %~nx0 C:\Logs
echo %~nx0 C:\Logs 30
echo %~nx0 C:\Logs 14 D:\Archive\Logs
endlocal
exit /b 1
)
if not "%~2"=="" set "DaysOld=%~2"
if not "%~3"=="" set "ArchiveDir=%~3"
:: Default archive directory is a subdirectory of the log directory
if not defined ArchiveDir set "ArchiveDir=%LogDir%\Archive"
if not exist "%LogDir%\" (
echo [ERROR] Log directory not found: %LogDir% >&2
endlocal
exit /b 1
)
:: Verify tar is available
where tar >nul 2>&1
if errorlevel 1 (
echo [ERROR] tar command not found. Windows 10 1803 or later required. >&2
echo [INFO] Use Method 3 (PowerShell Compress-Archive^) as a fallback.
endlocal
exit /b 1
)
:: Create archive directory
if not exist "%ArchiveDir%\" mkdir "%ArchiveDir%"
if not exist "%ArchiveDir%\" (
echo [ERROR] Could not create archive directory: %ArchiveDir% >&2
endlocal
exit /b 1
)
echo [INFO] Compressing log files older than %DaysOld% days in "%LogDir%"
echo [INFO] Archive destination: %ArchiveDir%
echo --------------------------------------------------
set "Compressed=0"
set "Skipped=0"
set "Failed=0"
:: Process .log files
forfiles /P "%LogDir%" /M *.log /D -%DaysOld% /C "cmd /c if @isdir==FALSE echo @path" 2>nul | (
for /f "delims=" %%f in ('more') do (
call :CompressFile "%%~f"
)
)
:: Also process .txt log files
forfiles /P "%LogDir%" /M *.txt /D -%DaysOld% /C "cmd /c if @isdir==FALSE echo @path" 2>nul | (
for /f "delims=" %%f in ('more') do (
call :CompressFile "%%~f"
)
)
echo --------------------------------------------------
echo [DONE] Compressed: !Compressed! Skipped: !Skipped! Failed: !Failed!
endlocal
exit /b 0
:CompressFile
setlocal
set "FilePath=%~1"
set "FileName=%~nx1"
set "ZipPath=%ArchiveDir%\%FileName%.zip"
:: Skip if archive already exists (prevents re-processing)
if exist "%ZipPath%" (
echo [SKIP] Already archived: %FileName%
endlocal
set /a "Skipped+=1"
exit /b 0
)
:: Attempt compression
echo [ZIP] %FileName%...
tar -acf "%ZipPath%" -C "%~dp1" "%FileName%" >nul 2>&1
if errorlevel 1 (
echo [FAIL] Could not compress: %FileName% (file may be locked^) >&2
:: Clean up partial archive
del "%ZipPath%" 2>nul
endlocal
set /a "Failed+=1"
exit /b 1
)
:: Verify the archive was created and is not empty
if not exist "%ZipPath%" (
echo [FAIL] Archive was not created: %FileName% >&2
endlocal
set /a "Failed+=1"
exit /b 1
)
for %%z in ("%ZipPath%") do (
if %%~zz equ 0 (
echo [FAIL] Archive is empty: %FileName% >&2
del "%ZipPath%" 2>nul
endlocal
set /a "Failed+=1"
exit /b 1
)
)
:: Delete original only after verified successful compression
del "%FilePath%" 2>nul
if errorlevel 1 (
echo [WARN] Compressed but could not delete original: %FileName% >&2
) else (
echo [OK] Archived: %FileName%
)
endlocal
set /a "Compressed+=1"
exit /b 0
How tar -acf works:
| Flag | Purpose |
|---|---|
-a | Auto-detect compression format from the output file extension (.zip → ZIP, .gz → gzip, .tar.gz → tar+gzip) |
-c | Create a new archive |
-f | Specify the output archive filename |
-C | Change to this directory before adding files (ensures the archive contains just the filename, not the full path) |
Why -C for the directory:
Without -C, tar stores the full path inside the archive. Extracting would recreate the entire directory structure. Using -C "%~dp1" "%FileName%" changes to the file's directory first and adds just the filename, producing a clean archive.
Why the archive is verified before deleting the original:
Three checks protect against data loss:
if errorlevel 1:tarreported a failure.if not exist "%ZipPath%": archive file was not created.if %%~zz equ 0: archive file exists but is empty (0 bytes), indicating a silent failure.
Only after all three checks pass is the original file deleted.
Method 2: Monthly Batch Archive
For environments that prefer one archive per month rather than per file, this method compresses all log files into a single timestamped archive at the end of each month.
@echo off
setlocal
set "LogDir=%~1"
set "ArchiveDir=%~dp0archives"
if "%LogDir%"=="" (
echo Usage: %~nx0 ^<log_directory^>
endlocal
exit /b 1
)
if not exist "%LogDir%\" (
echo [ERROR] Directory not found: %LogDir% >&2
endlocal
exit /b 1
)
where tar >nul 2>&1
if errorlevel 1 (
echo [ERROR] tar command not available. >&2
endlocal
exit /b 1
)
:: Generate locale-independent timestamp
for /f "delims=" %%t in (
'powershell -NoProfile -Command "Get-Date -Format ''yyyy-MM''"'
) do set "MonthStamp=%%t"
set "ArchiveName=%ArchiveDir%\logs_%MonthStamp%.zip"
if not exist "%ArchiveDir%\" mkdir "%ArchiveDir%"
:: Check if any log files exist
dir /b "%LogDir%\*.log" >nul 2>&1
if errorlevel 1 (
echo [INFO] No .log files found in %LogDir%.
endlocal
exit /b 0
)
echo [INFO] Creating monthly archive: %ArchiveName%
:: Compress all log files into one archive
tar -acf "%ArchiveName%" -C "%LogDir%" *.log >nul 2>&1
if errorlevel 1 (
echo [ERROR] Compression failed. Originals preserved. >&2
del "%ArchiveName%" 2>nul
endlocal
exit /b 1
)
:: Verify archive
if not exist "%ArchiveName%" (
echo [ERROR] Archive was not created. >&2
endlocal
exit /b 1
)
for %%z in ("%ArchiveName%") do (
if %%~zz equ 0 (
echo [ERROR] Archive is empty. Originals preserved. >&2
del "%ArchiveName%" 2>nul
endlocal
exit /b 1
)
)
:: Delete originals only after verified compression
echo [INFO] Removing original log files...
del "%LogDir%\*.log" 2>nul
echo [OK] Monthly archive created: %ArchiveName%
endlocal
exit /b 0
When to use per-file vs. monthly archives:
| Scenario | Recommended Method |
|---|---|
| Many small log files (< 1 MB each) | Method 2 - one archive per month reduces clutter |
| Few large log files (> 10 MB each) | Method 1 - per-file archives are easier to locate and extract |
| Compliance requires individual file retrieval | Method 1 - each file has its own named archive |
| Simple disk space recovery | Method 2 - fastest cleanup with least effort |
Method 3: PowerShell Fallback (No tar Available)
For older Windows versions (before 10 1803) where tar is not available, PowerShell's Compress-Archive provides equivalent functionality.
@echo off
setlocal
set "LogDir=%~1"
set "DaysOld=7"
set "ArchiveDir=%LogDir%\Archive"
if "%LogDir%"=="" (
echo Usage: %~nx0 ^<log_directory^> [days_old]
endlocal
exit /b 1
)
if not "%~2"=="" set "DaysOld=%~2"
if not exist "%LogDir%\" (
echo [ERROR] Directory not found: %LogDir% >&2
endlocal
exit /b 1
)
if not exist "%ArchiveDir%\" mkdir "%ArchiveDir%"
echo [INFO] Compressing logs older than %DaysOld% days (PowerShell method^)...
powershell -NoProfile -Command ^
"$cutoff = (Get-Date).AddDays(-%DaysOld%);" ^
"$logDir = '%LogDir%';" ^
"$archiveDir = '%ArchiveDir%';" ^
"$files = Get-ChildItem -Path $logDir -File -Include '*.log','*.txt' |" ^
" Where-Object { $_.LastWriteTime -lt $cutoff };" ^
"if (-not $files) { Write-Host 'No files older than %DaysOld% days.'; exit 0 };" ^
"$compressed = 0; $failed = 0;" ^
"foreach ($f in $files) {" ^
" $zipPath = Join-Path $archiveDir ($f.Name + '.zip');" ^
" if (Test-Path $zipPath) { Write-Host \" [SKIP] Already archived: $($f.Name)\"; continue };" ^
" try {" ^
" Compress-Archive -Path $f.FullName -DestinationPath $zipPath -Force;" ^
" if (Test-Path $zipPath) {" ^
" Remove-Item $f.FullName -Force;" ^
" Write-Host \" [OK] $($f.Name)\";" ^
" $compressed++" ^
" } else { $failed++; Write-Host \" [FAIL] $($f.Name)\" }" ^
" } catch {" ^
" $failed++;" ^
" Write-Host \" [FAIL] $($f.Name): $($_.Exception.Message)\"" ^
" }" ^
"};" ^
"Write-Host \"`nCompressed: $compressed Failed: $failed\""
endlocal
exit /b 0
Compress-Archive vs. tar:
| Feature | tar | Compress-Archive |
|---|---|---|
| Availability | Windows 10 1803+ | PowerShell 5.0+ (Windows 8.1+) |
| Speed | Faster (native C implementation) | Slower (managed .NET code) |
| Formats | ZIP, tar, tar.gz, tar.bz2 | ZIP only |
| File size limit | None practical | 2 GB per file in some PowerShell versions |
Method 4: Automatic Archive Cleanup
Compressed archives accumulate over time. This method deletes archives older than a configurable retention period.
@echo off
setlocal
set "ArchiveDir=%~1"
set "RetentionDays=365"
if "%ArchiveDir%"=="" (
echo Usage: %~nx0 ^<archive_directory^> [retention_days]
endlocal
exit /b 1
)
if not "%~2"=="" set "RetentionDays=%~2"
if not exist "%ArchiveDir%\" (
echo [ERROR] Directory not found: %ArchiveDir% >&2
endlocal
exit /b 1
)
echo [INFO] Removing archives older than %RetentionDays% days from %ArchiveDir%...
set "Deleted=0"
forfiles /P "%ArchiveDir%" /M *.zip /D -%RetentionDays% /C "cmd /c echo [DEL] @file & del @path" 2>nul
echo [OK] Archive cleanup complete.
endlocal
exit /b 0
Suggested retention schedule:
| Archive Age | Action |
|---|---|
| 0–30 days | Keep compressed archives, accessible for recent troubleshooting |
| 30–365 days | Keep compressed archives for historical reference |
| 365+ days | Delete (or move to cold storage/offsite backup) |
Adjust based on your compliance requirements. Some regulations (HIPAA, SOX, PCI-DSS) may require longer retention.
How to Avoid Common Errors
Wrong Way: Deleting Before Verifying Compression
:: DANGEROUS: deletes the original regardless of compression success
tar -acf archive.zip logfile.log
del logfile.log
If the disk is full, the file is locked, or tar encounters an error, the archive may be empty or corrupt. Deleting the original means the data is lost.
Correct Way: Check the exit code, verify the archive exists, and verify it is not empty before deleting the original. Method 1 demonstrates all three checks.
Problem: Locked Log Files
If an application is actively writing to a log file, tar cannot read it and will fail with an access error. The most common cause is trying to compress today's active log.
Solution: The age threshold (default: 7 days) ensures only old, inactive logs are compressed. If a log file is still locked despite being old (perhaps the application never closes it), the script skips it and tries again on the next run. Method 1 handles this with the [FAIL] Could not compress... (file may be locked) message.
Problem: tar Not Available on Older Windows
The tar command is included only in Windows 10 version 1803 and later.
Solution: Method 1 checks for tar availability with where tar and suggests Method 3 (PowerShell) as a fallback. Method 3 uses Compress-Archive, which is available in PowerShell 5.0+ (Windows 8.1 and later).
Problem: Date Format in Filenames
:: BROKEN on non-US locales - %date% format varies by regional settings
set "stamp=%date:~-4%%date:~4,2%%date:~7,2%"
Solution: Use PowerShell for locale-independent date formatting:
for /f "delims=" %%t in ('powershell -NoProfile -Command "Get-Date -Format ''yyyy-MM''"') do set "Stamp=%%t"
Problem: forfiles Cannot Call Subroutines Directly
:: BROKEN: forfiles runs in a separate cmd.exe context, cannot call :labels
forfiles /P "%LogDir%" /M *.log /D -7 /C "cmd /c call :CompressFile @path"
forfiles executes its /C command in a new cmd.exe instance that has no access to the calling script's subroutines.
Solution: Method 1 uses forfiles to output the file paths, then pipes them to a for /f loop in the main script context where subroutine calls work correctly.
Best Practices and Rules
1. Always Verify Before Deleting
Never delete the original file without confirming: (a) tar or Compress-Archive reported success, (b) the archive file exists, and (c) the archive is not empty (0 bytes).
2. Use a Separate Archive Directory
Store compressed files in a dedicated Archive subdirectory rather than alongside active logs. This keeps the log directory clean and makes retention policies (Method 4) easy to apply without affecting active files.
3. Include the Original Filename in the Archive Name
Name archives as originalname.log.zip so you know exactly what each archive contains without opening it. Avoid generic names like archive_001.zip.
4. Implement Archive Retention
Compressed archives still consume disk space. Use Method 4 or add forfiles /M *.zip /D -365 /C "cmd /c del @path" to your cleanup script to remove archives older than your retention requirement.
5. Schedule as a Weekly or Monthly Task
Run the compression script as a scheduled task during off-peak hours (e.g., Sunday 2 AM). This ensures logs are compressed regularly without manual intervention and minimizes the chance of encountering locked files.
6. Skip Already-Archived Files
Check whether an archive already exists before compressing. This prevents re-processing files that failed to delete on a previous run and avoids overwriting existing archives.
Conclusions
Automating the compression of old log files is a simple but powerful way to improve system reliability. By combining forfiles for age-based file selection, tar for compression, and rigorous verification before deletion, you create a self-managing logging system that provides historical data without the storage penalty of plain-text files. Adding archive retention cleanup ensures the compressed files themselves don't accumulate indefinitely.