PowerShell Windows Update Management Across Multiple Servers
─□✕

PowerShell Windows Update Management Across Multiple Servers

PowerShell Tips Editor 5 min read
PowerShell Windows Update Management Across Multiple Servers

The Case for Scripted Patching

Patching a server estate manually through Windows Update GUI or WSUS console is slow, inconsistent, and error-prone at scale. When maintenance windows are tight and servers number in the dozens, a sysadmin clicking through each machine risks missing systems, inconsistent patch selections, and no audit trail. The PSWindowsUpdate module combined with Invoke-Command lets you scan, approve, install, and report on updates across your entire fleet from a single management session, with a full log of what changed on each host.

Quick Answer

Install PSWindowsUpdate on each target server, use Get-WindowsUpdate to scan for missing updates, Install-WindowsUpdate -AcceptAll -AutoReboot to apply them, and wrap everything in Invoke-Command with -AsJob to run across multiple servers simultaneously without blocking on reboots.

Installing PSWindowsUpdate on Remote Machines

PSWindowsUpdate must be present on each target server, not just on the admin machine running the script. Use Invoke-Command to install it remotely via the PowerShell Gallery. If your servers do not have internet access, copy the module to a network share first and use -Source to install from there. Verify the install succeeded before proceeding to the scan phase.

$servers = @("SRV01","SRV02","SRV03","SRV04")

Invoke-Command -ComputerName $servers -ScriptBlock {
    if (-not (Get-Module -ListAvailable -Name PSWindowsUpdate)) {
        Install-PackageProvider -Name NuGet -Force -Scope AllUsers | Out-Null
        Install-Module -Name PSWindowsUpdate -Force -Scope AllUsers -ErrorAction Stop
        Write-Host "$env:COMPUTERNAME : PSWindowsUpdate installed"
    }
    else {
        Write-Host "$env:COMPUTERNAME : PSWindowsUpdate already present"
    }
}

Scanning for Missing Updates with Get-WindowsUpdate

Get-WindowsUpdate queries Windows Update or WSUS for available updates without installing them. Running a pre-patch scan across your farm gives you a complete picture of what will be applied before the maintenance window starts, allowing you to review critical vs optional updates and estimate reboot requirements. The output includes KB numbers, severity, and download size.

$scanResults = Invoke-Command -ComputerName $servers -ScriptBlock {
    Import-Module PSWindowsUpdate -ErrorAction Stop
    Get-WindowsUpdate -MicrosoftUpdate -NotTitle "Preview" |
        Select-Object ComputerName, KB, Title, Size, MsrcSeverity
}

$scanResults | Sort-Object ComputerName, MsrcSeverity |
    Format-Table -AutoSize

Write-Host "`nPending updates by server:"
$scanResults | Group-Object PSComputerName |
    Select-Object Name, Count | Format-Table -AutoSize

Installing Updates with Install-WindowsUpdate -AcceptAll

Install-WindowsUpdate -AcceptAll applies all available updates without interactive prompts. The -AutoReboot switch triggers a reboot automatically when required — omit it if you want to control reboot timing manually. Adding -NotTitle "Preview" skips optional preview releases that may introduce instability to production servers. Always log output to a file on the remote machine for post-patch verification.

Invoke-Command -ComputerName $servers -ScriptBlock {
    Import-Module PSWindowsUpdate -ErrorAction Stop
    $logPath = "C:\Logs\WindowsUpdate_$(Get-Date -Format yyyyMMdd_HHmm).log"

    Install-WindowsUpdate `
        -MicrosoftUpdate `
        -AcceptAll `
        -NotTitle "Preview" `
        -AutoReboot `
        -Verbose *>&1 | Tee-Object -FilePath $logPath

    Write-Host "$env:COMPUTERNAME : Update run complete. Log: $logPath"
}

Scheduling Reboots During Maintenance Windows

When -AutoReboot is too aggressive and you want precise control over reboot timing, schedule the reboot separately using shutdown.exe or Restart-Computer with a delay. This lets you stagger reboots across server groups to maintain service availability during patching — for example, patching the passive node of a cluster first, verifying services, then patching the active node.

# Install without auto-reboot first
Invoke-Command -ComputerName $servers -ScriptBlock {
    Import-Module PSWindowsUpdate -ErrorAction Stop
    Install-WindowsUpdate -AcceptAll -NotTitle "Preview" -IgnoreReboot
}

# Stagger reboots across two groups with 10-minute gap
$group1 = $servers[0..1]
$group2 = $servers[2..3]

Restart-Computer -ComputerName $group1 -Force -Wait -For WinRM -Timeout 300
Write-Host "Group 1 rebooted and online"

Start-Sleep -Seconds 600  # 10-minute gap
Restart-Computer -ComputerName $group2 -Force -Wait -For WinRM -Timeout 300
Write-Host "Group 2 rebooted and online"

Running Patch Installs Across Multiple Servers

Adding -AsJob to Invoke-Command lets updates install on all servers simultaneously. Without it, PowerShell waits for each server to complete before moving to the next. Using jobs means your script continues while patching runs in parallel, and you can collect results later with Receive-Job. This is especially important for long update runs that would otherwise time out the remote session.

$jobs = Invoke-Command -ComputerName $servers -AsJob -ScriptBlock {
    Import-Module PSWindowsUpdate -ErrorAction Stop
    Install-WindowsUpdate -AcceptAll -NotTitle "Preview" -AutoReboot -Confirm:$false
}

Write-Host "Patching jobs started: $($jobs.ChildJobs.Count)"

# Wait for all jobs with a 2-hour timeout
$jobs | Wait-Job -Timeout 7200

foreach ($job in $jobs.ChildJobs) {
    $result = Receive-Job -Job $job
    Write-Host "$($job.Location) : $(if($job.State -eq 'Completed'){'Done'}else{'Failed - check job'})"
}

Generating a Pre- and Post-Patch Compliance Report

Capturing the list of installed updates before and after the maintenance window produces a change record for compliance purposes and confirms that the intended updates were applied. Compare the two snapshots to identify any server that still has outstanding updates after patching, indicating a failed install or a missed reboot.

$postPatch = Invoke-Command -ComputerName $servers -ScriptBlock {
    Import-Module PSWindowsUpdate -ErrorAction Stop
    Get-WUHistory -MaxDate (Get-Date) |
        Where-Object { $_.Date -gt (Get-Date).AddHours(-4) } |
        Select-Object ComputerName, Date, Title, Result
}

$postPatch | Sort-Object PSComputerName, Date |
    Export-Csv "PatchReport_$(Get-Date -Format yyyyMMdd).csv" -NoTypeInformation

Write-Host "Post-patch report exported."

Common Errors

  • PSWindowsUpdate must be on the target server, not just the admin machine. Importing the module locally and running Get-WindowsUpdate against a remote computer name does not work. The module must be installed on each remote host and imported within the Invoke-Command script block. Verify installation with a remote Get-Module -ListAvailable check before the main script runs.
  • Server reboots during patching drop the remote session. When -AutoReboot triggers, the remote machine restarts and the Invoke-Command session terminates. Using -AsJob prevents the script from hanging, but the job state will show as failed rather than completed. Collect results from the local log file on the target machine instead.

Related Cmdlets / See Also

Wrapping Up

PSWindowsUpdate with Invoke-Command turns a tedious per-server maintenance task into a scripted, auditable, parallelized operation. Install the module on targets first, scan before applying, stagger reboots to maintain availability, and generate pre/post reports for compliance. The initial setup investment pays back every maintenance window thereafter.

Send-Item -To