How to Use BITS (Background Intelligent Transfer Service) in Batch Script
The Background Intelligent Transfer Service (BITS) is one of the most powerful hidden tools in Windows. It is the same engine that Windows Update uses to download patches. Unlike curl or copy, BITS transfers are intelligent: they only use idle network bandwidth (so they do not slow down the user's internet), they can resume automatically if the computer reboots or the Wi-Fi drops, and they run entirely in the background. For a Batch script, BITS is the gold standard for downloading large files or uploading backups in a way that has zero impact on the user's experience.
This guide will explain how to manage BITS jobs using the bitsadmin command.
Understanding the BITS Job Lifecycle
Before diving into scripts, it is essential to understand how BITS jobs work. A BITS job goes through a specific lifecycle that your scripts must follow:
- Create: A named job is registered with the BITS service.
- Add Files: One or more URL-to-local-path file pairs are added to the job.
- Resume: The job begins transferring in the background (jobs start in a suspended state).
- Monitor: The script checks progress by comparing bytes transferred to bytes total.
- Complete: When all bytes are transferred, the script must call
/completeto finalize the files at their destination paths.
The /complete step is critical and is the most commonly missed step. Until you call /complete, the downloaded file is held in a temporary location and is not available at the destination path. If you never complete the job, it eventually expires and is deleted, along with the downloaded data.
Method 1: Synchronous Download (Simple)
The /transfer command combines the entire lifecycle into a single blocking call. It creates the job, adds the file, starts the transfer, waits for completion, and finalizes it automatically. The script pauses until the download finishes.
@echo off
setlocal
set "URL=https://example.com/setup_package.zip"
set "DestFile=%~dp0setup_package.zip"
set "JobName=SimpleDownload_%RANDOM%"
rem --- Verify bitsadmin is available ---
where bitsadmin.exe >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] bitsadmin.exe is not available on this system.
endlocal
pause
exit /b 1
)
echo [BITS] Starting synchronous download...
echo [INFO] URL: %URL%
echo [INFO] Dest: %DestFile%
echo [INFO] This will block until the download completes.
echo.
rem --- /transfer is synchronous: it blocks until finished ---
rem --- /priority normal uses idle bandwidth by default ---
bitsadmin /transfer "%JobName%" /priority normal "%URL%" "%DestFile%"
if %errorlevel% neq 0 (
echo.
echo [ERROR] BITS transfer failed.
echo [INFO] Common causes:
echo - The URL is invalid or returns a non-200 HTTP status.
echo - The destination path is not an absolute local path.
echo - The BITS service is not running.
echo - No internet connectivity.
endlocal
pause
exit /b 1
)
rem --- Verify the file actually exists at the destination ---
if exist "%DestFile%" (
echo.
echo [SUCCESS] Download complete: %DestFile%
for %%f in ("%DestFile%") do echo [INFO] File size: %%~zf bytes
) else (
echo.
echo [ERROR] Transfer reported success but file was not found.
)
endlocal
pause
Key details:
- Absolute paths required: BITS requires the destination to be a fully qualified local path (e.g.,
C:\Downloads\file.zip). Relative paths like.\file.zipwill cause the command to fail. Using%~dp0(the script's own directory) ensures an absolute path. %RANDOM%in job name: If the script is run multiple times, using a unique job name prevents "job already exists" errors.- File verification: Even though
bitsadmin /transferreturnserrorlevel 0on success, the script confirms the file exists at the destination as a final safeguard.
Synchronous blocking: The /transfer command blocks the script until the download finishes completely. For a 2 GB file on a slow connection, this could take hours. If you need the script to continue immediately while the download runs in the background, use Method 2.
Method 2: Asynchronous Download with Monitoring
This is the professional pattern for BITS. You create a job, start it in the background, and then monitor its progress. The job continues even if the script exits, the user logs off, or the computer reboots (the job resumes automatically after restart).
Step 1: Create and Start the Job
@echo off
setlocal
set "JobName=BackgroundUpdate"
set "URL=https://example.com/large_update.zip"
set "DestFile=C:\Downloads\large_update.zip"
rem --- Admin check: creating jobs for monitoring across sessions ---
rem --- may require elevation depending on the scenario ---
rem --- Basic BITS jobs work without admin for the current user ---
rem --- Verify bitsadmin is available ---
where bitsadmin.exe >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] bitsadmin.exe is not available.
endlocal
pause
exit /b 1
)
rem --- Ensure the destination directory exists ---
for %%f in ("%DestFile%") do (
if not exist "%%~dpf" mkdir "%%~dpf"
)
rem --- Cancel any existing job with the same name to start fresh ---
bitsadmin /cancel "%JobName%" >nul 2>&1
rem --- Create the job (starts in SUSPENDED state) ---
echo [BITS] Creating job "%JobName%"...
bitsadmin /create "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Failed to create BITS job.
endlocal
pause
exit /b 1
)
rem --- Add the file to the job ---
echo [BITS] Adding file...
echo URL: %URL%
echo Dest: %DestFile%
bitsadmin /addfile "%JobName%" "%URL%" "%DestFile%"
if %errorlevel% neq 0 (
echo [ERROR] Failed to add file to BITS job.
echo [INFO] Cancelling job...
bitsadmin /cancel "%JobName%" >nul 2>&1
endlocal
pause
exit /b 1
)
rem --- Set priority (optional) ---
rem --- foreground = full speed, normal = balanced, low = minimal impact ---
bitsadmin /setpriority "%JobName%" normal >nul 2>&1
rem --- Start the transfer ---
echo [BITS] Starting transfer...
bitsadmin /resume "%JobName%"
if %errorlevel% neq 0 (
echo [ERROR] Failed to start BITS job.
bitsadmin /cancel "%JobName%" >nul 2>&1
endlocal
pause
exit /b 1
)
echo.
echo [SUCCESS] Job "%JobName%" is now transferring in the background.
echo [INFO] The download will continue even if you close this window.
echo [INFO] Run the monitoring script (Step 2) to check progress.
endlocal
pause
Step 2: Monitor Progress and Complete
This script can be run at any time, even after a reboot, to check the status of the background job and finalize it when done.
@echo off
setlocal enabledelayedexpansion
set "JobName=BackgroundUpdate"
rem --- Verify the job exists by attempting to get its state ---
rem --- bitsadmin returns non-zero errorlevel if the job does not exist ---
bitsadmin /getstate "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [INFO] No BITS job named "%JobName%" found.
echo [INFO] The job may have already been completed or cancelled.
endlocal
pause
exit /b 0
)
echo [BITS] Monitoring job "%JobName%"...
echo [INFO] Press Ctrl+C to stop monitoring (the download will continue).
echo.
:PollLoop
rem --- Get bytes transferred and bytes total ---
rem --- These commands return raw numbers, which are language-independent ---
set "BytesDone="
set "BytesTotal="
for /f "tokens=*" %%a in ('bitsadmin /getbytestransferred "%JobName%" 2^>nul') do (
set "BytesDone=%%a"
)
for /f "tokens=*" %%a in ('bitsadmin /getbytestotal "%JobName%" 2^>nul') do (
set "BytesTotal=%%a"
)
rem --- Check if the job still exists (may have been cancelled externally) ---
bitsadmin /getstate "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [INFO] Job "%JobName%" no longer exists.
goto :End
)
rem --- Display progress ---
if defined BytesDone if defined BytesTotal (
echo [!time!] Progress: !BytesDone! / !BytesTotal! bytes
)
rem --- Check if transfer is complete (bytes match and total is not zero) ---
rem --- We also attempt /complete; if the job is not in TRANSFERRED state, ---
rem --- /complete returns non-zero and we continue polling ---
bitsadmin /complete "%JobName%" >nul 2>&1
if !errorlevel! equ 0 (
echo.
echo [SUCCESS] Job "%JobName%" completed and finalized.
echo [INFO] The file is now available at its destination.
goto :End
)
rem --- Check for errors by attempting to get error info ---
rem --- If /geterror returns specific error text, the job has failed ---
rem --- Rather than parsing localized text, we check if the job ---
rem --- has been in the same state for too long, or use /getstate ---
rem --- combined with /complete failure to determine if still transferring ---
rem --- Wait before checking again ---
timeout /t 5 /nobreak >nul
goto :PollLoop
:End
endlocal
pause
How the monitoring avoids language-dependent parsing:
The scripts searched for text like "ERROR" in bitsadmin /info output, which is translated on non-English Windows. The fixed monitoring script uses a different approach:
/getbytestransferredand/getbytestotalreturn raw numbers, language-independent./completeis attempted on every poll cycle. If the job is in theTRANSFERREDstate (all bytes received),/completesucceeds and finalizes the file. If the job is still transferring or has errors,/completefails (returns non-zero errorlevel) and the loop continues./getstateis used only to check if the job still exists (non-zero errorlevel means the job was deleted or already completed), not to parse the state text.
Method 3: Error Recovery and Cleanup
BITS jobs that fail (bad URL, server error, network loss) stay in the system indefinitely until explicitly cancelled. Over time, abandoned jobs accumulate and waste resources. This script checks for and cleans up problematic jobs.
@echo off
setlocal enabledelayedexpansion
set "JobName=BackgroundUpdate"
rem --- Check if the job exists ---
bitsadmin /getstate "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [INFO] No job named "%JobName%" found. Nothing to clean up.
endlocal
pause
exit /b 0
)
echo [BITS] Checking job "%JobName%"...
rem --- Try to complete the job (works if transfer is finished) ---
bitsadmin /complete "%JobName%" >nul 2>&1
if %errorlevel% equ 0 (
echo [SUCCESS] Job was finished and has been completed.
endlocal
pause
exit /b 0
)
rem --- If /complete failed, the job is either still transferring or errored ---
rem --- Check if any bytes have been transferred ---
set "BytesDone=0"
for /f "tokens=*" %%a in ('bitsadmin /getbytestransferred "%JobName%" 2^>nul') do (
set "BytesDone=%%a"
)
set "BytesTotal=0"
for /f "tokens=*" %%a in ('bitsadmin /getbytestotal "%JobName%" 2^>nul') do (
set "BytesTotal=%%a"
)
echo [INFO] Bytes transferred: !BytesDone! / !BytesTotal!
rem --- Offer options to the user ---
echo.
echo [OPTIONS]
echo 1. Resume the job (retry transfer)
echo 2. Cancel the job (delete it)
echo 3. Do nothing (leave it as-is)
echo.
set /p "Choice=Enter choice (1/2/3): "
if "%Choice%"=="1" (
echo [BITS] Resuming job...
bitsadmin /resume "%JobName%"
if !errorlevel! equ 0 (
echo [OK] Job resumed. Run the monitoring script to track progress.
) else (
echo [ERROR] Could not resume. The job may be in an unrecoverable error state.
echo [INFO] Consider cancelling with option 2.
)
) else if "%Choice%"=="2" (
echo [BITS] Cancelling job...
bitsadmin /cancel "%JobName%"
if !errorlevel! equ 0 (
echo [OK] Job cancelled and removed from the system.
) else (
echo [ERROR] Could not cancel the job.
)
) else (
echo [INFO] No action taken.
)
endlocal
pause
Method 4: Listing All BITS Jobs
To see all active BITS jobs (useful for finding abandoned or stuck jobs), use /list. This script provides a language-independent way to check if any jobs exist.
@echo off
setlocal
echo [BITS] Current BITS jobs:
echo ========================
echo.
rem --- /list shows jobs for the current user ---
rem --- /allusers shows all jobs (requires admin) ---
bitsadmin /list 2>nul
echo.
rem --- Check if running as admin for /allusers view ---
net session >nul 2>&1
if %errorlevel% equ 0 (
echo [ADMIN] All users' jobs:
echo ========================
echo.
bitsadmin /list /allusers 2>nul
)
endlocal
pause
Method 5: Complete Production Download Script
This combined script handles the full lifecycle: creating, monitoring, completing, and error handling, in a single file.
@echo off
setlocal enabledelayedexpansion
rem ==============================
rem Configuration
rem ==============================
set "URL=https://example.com/large_file.zip"
set "DestFile=%~dp0downloads\large_file.zip"
set "JobName=ProdDownload_%RANDOM%"
set "Priority=normal"
set "PollInterval=5"
set "MaxWaitSeconds=3600"
rem ==============================
rem Pre-Flight Checks
rem ==============================
rem --- Verify bitsadmin is available ---
where bitsadmin.exe >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] bitsadmin.exe is not available on this system.
endlocal
pause
exit /b 1
)
rem --- Ensure destination directory exists ---
for %%f in ("%DestFile%") do (
if not exist "%%~dpf" (
mkdir "%%~dpf" 2>nul
if !errorlevel! neq 0 (
echo [ERROR] Cannot create destination directory: %%~dpf
endlocal
pause
exit /b 1
)
)
)
rem --- Remove destination file if it already exists ---
if exist "%DestFile%" (
echo [INFO] Removing existing file at destination...
del "%DestFile%" >nul 2>&1
)
rem ==============================
rem Create and Start Job
rem ==============================
echo [BITS] Creating download job...
echo URL: %URL%
echo Dest: %DestFile%
echo Priority: %Priority%
echo.
bitsadmin /create "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Failed to create BITS job.
endlocal
pause
exit /b 1
)
bitsadmin /addfile "%JobName%" "%URL%" "%DestFile%" >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Failed to add file to job. URL or path may be invalid.
bitsadmin /cancel "%JobName%" >nul 2>&1
endlocal
pause
exit /b 1
)
bitsadmin /setpriority "%JobName%" %Priority% >nul 2>&1
bitsadmin /resume "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Failed to start job.
bitsadmin /cancel "%JobName%" >nul 2>&1
endlocal
pause
exit /b 1
)
echo [BITS] Transfer started. Monitoring progress...
echo.
rem ==============================
rem Monitor Progress
rem ==============================
set "Elapsed=0"
:MonitorLoop
rem --- Check if the job still exists ---
bitsadmin /getstate "%JobName%" >nul 2>&1
if %errorlevel% neq 0 (
echo [ERROR] Job disappeared unexpectedly.
endlocal
pause
exit /b 1
)
rem --- Try to complete (succeeds only if all bytes transferred) ---
bitsadmin /complete "%JobName%" >nul 2>&1
if !errorlevel! equ 0 (
echo.
echo [SUCCESS] Download complete!
goto :Verify
)
rem --- Get progress numbers ---
set "Done=0"
set "Total=0"
for /f "tokens=*" %%a in ('bitsadmin /getbytestransferred "%JobName%" 2^>nul') do set "Done=%%a"
for /f "tokens=*" %%a in ('bitsadmin /getbytestotal "%JobName%" 2^>nul') do set "Total=%%a"
echo [!time!] !Done! / !Total! bytes transferred
rem --- Check timeout ---
set /a Elapsed+=PollInterval
if !Elapsed! geq %MaxWaitSeconds% (
echo.
echo [TIMEOUT] Download exceeded %MaxWaitSeconds% seconds.
echo [INFO] The job is still running in the background.
echo [INFO] Run this script again later to resume monitoring.
echo [INFO] To cancel: bitsadmin /cancel "%JobName%"
endlocal
pause
exit /b 1
)
timeout /t %PollInterval% /nobreak >nul
goto :MonitorLoop
rem ==============================
rem Verify Download
rem ==============================
:Verify
if exist "%DestFile%" (
for %%f in ("%DestFile%") do (
echo File: %DestFile%
echo Size: %%~zf bytes
)
echo.
echo [DONE] File is ready at: %DestFile%
) else (
echo [ERROR] Job completed but file was not found at destination.
)
endlocal
pause
How to Avoid Common Errors
Wrong Way: Forgetting to Call /complete
This is the single most common BITS mistake. When all bytes are transferred, the job enters a TRANSFERRED state, but the file remains in a temporary BITS cache. It is not written to the destination path until you call /complete.
rem *** BAD: file will never appear at C:\Downloads\ ***
bitsadmin /create MyJob
bitsadmin /addfile MyJob "https://example.com/file.zip" "C:\Downloads\file.zip"
bitsadmin /resume MyJob
rem ... time passes, download finishes ...
rem Where is my file?! (answer: stuck in BITS cache forever)
Correct Way: Always call /complete after the transfer finishes.
rem *** GOOD: file is finalized at the destination ***
bitsadmin /complete MyJob
Wrong Way: Using Relative Paths
BITS requires fully qualified absolute paths for the destination. Relative paths cause immediate failure.
rem *** BAD: relative path causes failure ***
bitsadmin /addfile MyJob "https://example.com/file.zip" ".\file.zip"
rem *** BAD: current directory may not be what you expect ***
bitsadmin /addfile MyJob "https://example.com/file.zip" "file.zip"
Correct Way: Always use absolute paths. %~dp0 (script directory) or %CD% (current directory, expanded to absolute) are reliable options.
rem *** GOOD: absolute path ***
bitsadmin /addfile MyJob "https://example.com/file.zip" "%~dp0file.zip"
Wrong Way: Parsing Localized State Text
The bitsadmin /info and /getstate output text is translated on non-English Windows. Searching for English state names like "ERROR" or "TRANSFERRED" fails on other languages.
rem *** BAD: "ERROR" is translated on non-English Windows ***
bitsadmin /info MyJob | findstr "ERROR" >nul
Correct Way: Use language-independent checks: attempt /complete (which only succeeds in the TRANSFERRED state), compare byte counts, or check errorlevel values.
rem *** GOOD: /complete succeeds only when transfer is done ***
bitsadmin /complete MyJob >nul 2>&1
if %errorlevel% equ 0 (echo Done) else (echo Still working...)
Wrong Way: Reusing Job Names Without Cleanup
If a job with the same name already exists (from a previous failed run), creating a new job with that name fails.
rem *** BAD: fails if "MyJob" already exists ***
bitsadmin /create MyJob
Correct Way: Cancel any existing job with the same name before creating a new one, or use unique names with %RANDOM%.
rem *** GOOD: clean slate ***
bitsadmin /cancel MyJob >nul 2>&1
bitsadmin /create MyJob
BITS Priority Levels
| Priority | Behavior | Use Case |
|---|---|---|
foreground | Uses maximum available bandwidth | User is waiting for the file |
high | High priority but yields to foreground traffic | Important automated downloads |
normal | Balanced, i.e. uses idle bandwidth | Standard background transfers |
low | Minimal bandwidth usage | Maintenance tasks during business hours |
Best Practices and Rules
1. Always Complete or Cancel Every Job
A BITS job that is neither completed nor cancelled stays in the system indefinitely (up to 90 days by default). Over time, abandoned jobs accumulate. Every script that creates a job should have a clear path to either /complete or /cancel it.
2. Use Absolute Paths for Destinations
BITS does not resolve relative paths. Always provide a fully qualified path like C:\Downloads\file.zip. Use %~dp0 to reference the script's own directory.
3. The bitsadmin Command Is Language-Independent
The command name and all its switches (/transfer, /create, /addfile, /resume, /complete, /cancel, /getbytestransferred, /getbytestotal) are identical across all Windows display languages. Only the descriptive output text (state names, error messages) is translated. Use errorlevel checks and byte count comparisons instead of parsing output text.
4. BITS Handles Network Interruptions Automatically
If the network drops during a transfer, BITS automatically pauses and resumes when connectivity returns. If the computer reboots, the job persists and continues after startup. You do not need to build retry logic, BITS handles this natively.
5. Job Ownership Matters
A BITS job is owned by the user who created it. A standard user cannot see or manage jobs created by an Administrator, and vice versa. Use /allusers (requires admin) with /list to see all jobs across accounts.
6. bitsadmin Deprecation Notice
Microsoft has marked bitsadmin.exe as deprecated in favor of PowerShell's Start-BitsTransfer cmdlet. However, bitsadmin is still present and functional in all current Windows versions (including Windows 11) and remains the most practical option for Batch scripts where calling PowerShell adds complexity.
7. Use setlocal / endlocal
Always wrap scripts in setlocal and endlocal to prevent variables from leaking into the parent environment.
Final Thoughts
BITS is the ultimate background transfer engine for Windows automation. Unlike curl or copy, it uses only idle bandwidth, survives network interruptions and reboots, and runs silently without impacting the user's experience. The critical points for reliable BITS scripting are: always use absolute destination paths, always call /complete to finalize transfers (or /cancel to clean up failures), use unique job names to avoid conflicts, and avoid parsing localized output text. Instead, check errorlevel values and byte counts, which are language-independent. Despite its deprecated status, bitsadmin remains the most practical way to leverage the BITS engine from Batch scripts and continues to work on all current Windows versions.