PowerShell Download File from URL: Multiple Methods

PowerShell Download File from URL: Multiple Methods

PowerShell Tips Editor 5 min read
PowerShell Download File from URL: Multiple Methods

Bootstrapping a deployment means downloading the installer before you can run it — and doing it reliably across a fleet of machines requires more than opening a browser. When you need to PowerShell download file from URL, you have four solid approaches depending on whether you need simplicity, a progress bar, background downloading for large files, or JSON from an API endpoint. This post covers all four with working examples and common gotchas to skip.

Quick Answer / TL;DR

For most downloads: Invoke-WebRequest -Uri "https://example.com/file.zip" -OutFile "C:\Temp\file.zip". For speed in scripts, add -UseBasicParsing.

Method 1: Invoke-WebRequest

Invoke-WebRequest is the most common choice. It handles redirects, cookies, and custom headers. The -OutFile parameter streams the response directly to disk — no need to load the entire file into memory first.

Invoke-WebRequest -Uri "https://example.com/installer.msi" `
    -OutFile "C:\Temp\installer.msi" `
    -UseBasicParsing

-UseBasicParsing skips HTML DOM parsing, making the download significantly faster and avoiding errors on systems where Internet Explorer is not initialized. Always include it in scripts.

# With custom headers (e.g., Authorization)
Invoke-WebRequest -Uri "https://api.example.com/artifact.zip" `
    -Headers @{ Authorization = "Bearer $token" } `
    -OutFile "C:\Temp\artifact.zip" `
    -UseBasicParsing

Method 2: WebClient .DownloadFile()

The .NET WebClient class is fast and lightweight. It is synchronous by default and does not show a progress bar, making it ideal for scripts where you just need the file on disk without any overhead.

$client = [System.Net.WebClient]::new()
$client.DownloadFile(
    "https://example.com/package.zip",
    "C:\Temp\package.zip"
)
$client.Dispose()

WebClient is notably faster than Invoke-WebRequest for large files because it does not build a response object in memory. Call Dispose() afterward to release the connection.

Method 3: BITS Transfer for Large Files

Background Intelligent Transfer Service (BITS) is the engine behind Windows Update. It downloads files in the background, survives network interruptions, and throttles bandwidth to avoid impacting other traffic. Use Start-BitsTransfer for files over a few hundred megabytes or when the download needs to run while users are working.

# Synchronous BITS download
Start-BitsTransfer -Source "https://example.com/largefile.iso" `
    -Destination "C:\Temp\largefile.iso"

# Asynchronous (background) BITS download
$job = Start-BitsTransfer -Source "https://example.com/largefile.iso" `
    -Destination "C:\Temp\largefile.iso" `
    -Asynchronous

# Wait for completion
while ($job.JobState -eq "Transferring") {
    Write-Output "Transferred: $($job.BytesTransferred / 1MB) MB"
    Start-Sleep -Seconds 5
}
Complete-BitsTransfer -BitsJob $job

Method 4: Invoke-RestMethod for APIs

When downloading from a REST API that returns JSON or XML, Invoke-RestMethod is the right tool — it automatically parses the response body into a PowerShell object. For binary files from an API (like an artifact from a CI system), stick with Invoke-WebRequest -OutFile.

# Download and parse JSON in one step
$data = Invoke-RestMethod -Uri "https://api.example.com/releases/latest"
Write-Output "Latest version: $($data.tag_name)"

# Download binary artifact URL from JSON response
$downloadUrl = $data.assets[0].browser_download_url
Invoke-WebRequest -Uri $downloadUrl -OutFile "C:\Temp\release.zip" -UseBasicParsing

Show Progress During Download

Invoke-WebRequest shows a built-in progress bar, but it slows the download significantly due to rendering overhead. Use the WebClient DownloadProgressChanged event for real progress without the performance hit, or simply check the file size as it grows.

# Simple size-based progress check
$url  = "https://example.com/bigfile.zip"
$dest = "C:\Temp\bigfile.zip"

$client = [System.Net.WebClient]::new()
$client.DownloadFileAsync([Uri]$url, $dest)

while ($client.IsBusy) {
    $size = if (Test-Path $dest) { (Get-Item $dest).Length / 1MB } else { 0 }
    Write-Output "Downloaded: $([math]::Round($size, 1)) MB"
    Start-Sleep -Seconds 2
}
$client.Dispose()
Write-Output "Download complete."

Retry on Failure

Network-dependent scripts should retry on failure. Wrap the download in a loop with a configurable retry count and exponential backoff.

function Invoke-DownloadWithRetry {
    param(
        [string]$Uri,
        [string]$OutFile,
        [int]$MaxRetries = 3
    )

    for ($attempt = 1; $attempt -le $MaxRetries; $attempt++) {
        try {
            Invoke-WebRequest -Uri $Uri -OutFile $OutFile -UseBasicParsing -ErrorAction Stop
            Write-Output "Downloaded successfully on attempt $attempt"
            return
        } catch {
            Write-Warning "Attempt $attempt failed: $_"
            Start-Sleep -Seconds ($attempt * 5)
        }
    }
    throw "Download failed after $MaxRetries attempts: $Uri"
}

Invoke-DownloadWithRetry -Uri "https://example.com/file.zip" -OutFile "C:\Temp\file.zip"

Common Errors and Fixes

  • Invoke-WebRequest slow due to progress rendering: The default progress bar in Invoke-WebRequest calls the Windows GUI rendering engine on every chunk, which can reduce throughput to a fraction of your actual bandwidth. Always add -UseBasicParsing in scripts. You can also set $ProgressPreference = 'SilentlyContinue' to disable progress globally for the session.
  • BITS not available in all environments: BITS is a Windows service that must be running. In some stripped-down Server Core installs or container environments, the BITS service may be disabled or absent. Check with Get-Service -Name BITS before relying on Start-BitsTransfer. Fall back to Invoke-WebRequest or WebClient if BITS is unavailable.

Related Cmdlets / See Also

Wrapping Up

For quick scripts use Invoke-WebRequest -UseBasicParsing; for large files use BITS; for .NET speed use WebClient. As a next step, wrap your chosen method in the retry function above and add a Test-Path check before downloading so reruns skip files that already exist on disk.

Send-Item -To