SCRIPT LIBRARY · POWERSHELL
Clearing a Stuck Print Spooler with PowerShell
Stop the spooler, clear out the wedged job files, and bring it back up, with -WhatIf and an option to leave recent jobs alone.
- What it does
- Stops the Print Spooler service, deletes the queued .SPL and .SHD job files from the spool folder, and starts the service (and anything that depends on it) again. Can limit the cleanup to jobs older than a set number of minutes.
- Requires
- Windows PowerShell 5.1 or PowerShell 7+
- No modules
- Permissions
- Local admin, elevated. Runs fine as SYSTEM from a deployment tool.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and dry-run with mocked cmdlets in PowerShell 7.4
Part 5 of the thread Spring cleaning for Windows PCs
One job gets stuck at the top of the queue, and everything behind it just sits there. Users hit Print again. And again. By the time the ticket comes in there are fourteen copies of the same PDF waiting behind a job that's never going to finish.
The fix everybody knows is stop the spooler, delete the files in spool\PRINTERS, start the spooler. It works. This script does the same thing with a few guard rails: it reads the actual spool folder from the registry instead of assuming, it always restarts the service even if something goes wrong halfway, it brings dependent services back up too, and it can leave recent jobs alone so the person who printed thirty seconds ago doesn't lose their document.
The original post wrapped a three-line version of this in a script that built a ConfigMgr package. The package part is easy to do by hand and it's covered at the bottom; the script that runs on the machine is the part worth getting right.
<#
.SYNOPSIS
Clears stuck print jobs by stopping the Print Spooler, deleting its queued job files,
and starting it again.
.DESCRIPTION
Reads the real spool folder from the registry (it isn't always C:\Windows\System32\spool\PRINTERS),
stops the Spooler service, removes the .SPL/.SHD job files, and restarts the service.
With -OlderThanMinutes it leaves recent jobs alone and only clears the old, wedged ones.
The spooler is started again even if the cleanup fails partway. Supports -WhatIf.
.PARAMETER OlderThanMinutes
Only delete job files older than this. 0 (the default) clears the whole queue.
.PARAMETER TimeoutSeconds
How long to wait for the service to stop or start before giving up. Default: 30.
.EXAMPLE
.\Clear-PrintSpooler.ps1 -WhatIf
.EXAMPLE
.\Clear-PrintSpooler.ps1 -OlderThanMinutes 30
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateRange(0, 10080)]
[int]$OlderThanMinutes = 0,
[ValidateRange(5, 300)]
[int]$TimeoutSeconds = 30
)
$ErrorActionPreference = 'Stop'
$printKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Print\Printers'
$spoolDir = (Get-ItemProperty -Path $printKey -Name DefaultSpoolDirectory -ErrorAction SilentlyContinue).DefaultSpoolDirectory
if (-not $spoolDir) { $spoolDir = Join-Path $env:SystemRoot 'System32\spool\PRINTERS' }
Write-Verbose "Spool folder: $spoolDir"
$cutoff = (Get-Date).AddMinutes(-$OlderThanMinutes)
$jobFiles = @(Get-ChildItem -LiteralPath $spoolDir -File -Force -ErrorAction SilentlyContinue |
Where-Object { $_.Extension -in '.SPL', '.SHD' -and ($OlderThanMinutes -eq 0 -or $_.LastWriteTime -lt $cutoff) })
$result = [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
SpoolFolder = $spoolDir
FilesFound = $jobFiles.Count
FilesRemoved = 0
SpoolerState = (Get-Service -Name Spooler).Status.ToString()
Error = $null
}
if ($jobFiles.Count -eq 0) {
Write-Verbose 'No matching job files. Nothing to clear.'
return $result
}
if (-not $PSCmdlet.ShouldProcess("$($jobFiles.Count) job file(s) in $spoolDir", 'Stop Spooler, delete, restart')) {
return $result
}
$spooler = Get-Service -Name Spooler
$wasRunning = $spooler.Status -eq 'Running'
# Remember which dependent services were running, so they can be brought back too.
$dependents = @($spooler.DependentServices | Where-Object Status -eq 'Running')
try {
if ($wasRunning) {
# -Force also stops anything that depends on the spooler (fax, some vendor print services).
Stop-Service -InputObject $spooler -Force -Confirm:$false
$spooler.WaitForStatus('Stopped', [timespan]::FromSeconds($TimeoutSeconds))
}
foreach ($file in $jobFiles) {
try {
Remove-Item -LiteralPath $file.FullName -Force -Confirm:$false
$result.FilesRemoved++
}
catch {
Write-Warning "Couldn't delete $($file.Name): $($_.Exception.Message)"
}
}
}
catch {
$result.Error = $_.Exception.Message
}
finally {
# Whatever happened above, don't leave the machine without a spooler.
if ($wasRunning) {
try {
Start-Service -InputObject $spooler -Confirm:$false
$spooler.WaitForStatus('Running', [timespan]::FromSeconds($TimeoutSeconds))
foreach ($svc in $dependents) { Start-Service -InputObject $svc -Confirm:$false -ErrorAction Continue }
}
catch {
$result.Error = "Spooler didn't come back up: $($_.Exception.Message)"
}
}
$spooler.Refresh()
$result.SpoolerState = $spooler.Status.ToString()
}
$result
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-OlderThanMinutes | int | 0 | Only delete job files older than this many minutes. 0 clears the whole queue. |
-TimeoutSeconds | int | 30 | How long to wait for the spooler to stop or start before treating it as a failure. |
Run it
See what would be cleared, without stopping anything.
.\Clear-PrintSpooler.ps1 -WhatIfClear everything. The classic fix.
.\Clear-PrintSpooler.ps1Only clear jobs that have been stuck for more than 30 minutes.
.\Clear-PrintSpooler.ps1 -OlderThanMinutes 30Run it on a remote PC.
Invoke-Command -ComputerName PC-0142 -FilePath .\Clear-PrintSpooler.ps1What you'll see
ComputerName : PC-0142
SpoolFolder : C:\Windows\System32\spool\PRINTERS
FilesFound : 28
FilesRemoved : 28
SpoolerState : Running
Error :
How it works
- Find the real spool folder. It's read from
DefaultSpoolDirectoryunderHKLM\SYSTEM\CurrentControlSet\Control\Print\Printers, with the usual default as a fallback. - Pick the job files. Each queued job is a
.SPLfile (the data) and a.SHDfile (the shadow file with the job details). With-OlderThanMinutes, only files older than the cutoff are picked. If there's nothing to clear, the service is never touched. - Stop, delete, start. The spooler is stopped with
-Force(which also stops anything that depends on it), the files are deleted one by one, and the service comes back. A file that won't delete is a warning, not a reason to give up. - Always restart. The restart sits in a
finallyblock, so even if the delete step blows up, the machine isn't left without a spooler. Dependent services that were running before get started again too.
Deploying it with ConfigMgr
If you want a button for the service desk, this works well as a ConfigMgr Script (Software Library > Scripts), which runs on demand against a device or collection and shows the output right in the console. It'll need to be approved before anyone can run it.
For a package and program instead, copy the script to your source share (say \\sccm01\Sources\Scripts\Clear-PrintSpooler), create a package from that folder, and use this as the program command line, set to run with administrative rights whether or not a user is logged on:
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Clear-PrintSpooler.ps1
Intune works the same way as a platform script or a remediation.
Take it further
- Make it a remediation. Detect "any job file older than an hour" and remediate with
-OlderThanMinutes 60. Stuck queues fix themselves before anyone calls. - Log what you cleared. Pipe the result to
Export-Csv -Appendon a share if you want to spot the machines that jam every week.
Things that'll trip you up
- Everything in the queue goes. Without -OlderThanMinutes, every pending job on every printer on that machine is deleted. On a print server that's everyone's jobs, not just the stuck one. Use the age filter, or clear the single job from the queue instead.
- The spooler folder can be moved. Print servers often put the spool folder on another drive. The script reads DefaultSpoolDirectory from the registry, so it follows the move. Hard-coded C:\Windows\System32\spool\PRINTERS paths don't.
- If it keeps coming back, it's the driver. A spooler that jams every day is usually a bad printer driver or a corrupt job from one particular app. Clearing the queue treats the symptom. Check the PrintService event logs and update or swap the driver.
- Try the gentle way first on servers. Get-PrintJob -PrinterName 'Floor2-Copier' | Where-Object JobStatus -like '*Error*' | Remove-PrintJob removes just the broken jobs without restarting the service for everyone.