Shore Up
A jammed paper tray on an office printer being cleared and reset, with a stack of stuck pages sliding out and the tray running clean again.
Windows

Automate Print-Queue Cleanup and Clearing Stuck Jobs

Ketan Aagja9 min read
No ratings yet

Stuck print jobs are one of those recurring, low-glamour tickets: a queue jams, one document sits in "Error" or "Deleting" forever, and everything behind it stalls. This guide gives you two standard tools — a graceful per-job cleanup and the classic spooler reset — and shows how to run them safely on a schedule.

Before you run this

What this does: The scripts below remove print jobs from Windows print queues. The first, gentle version cancels jobs older than a chosen age through the normal print API. The second, harder reset stops the Print Spooler service, deletes the spooled job files on disk, and restarts the service — this is the "nuclear" option for jobs that refuse to cancel.

Privileges: Both require an elevated PowerShell session (Run as Administrator). Managing other users' print jobs, and stopping/starting the Spooler service, needs administrative rights on the print server. On a domain print server that means a member of the local Administrators group.

This is destructive and not reversible. Cancelling a print job discards it — the user must resubmit. The spooler reset deletes every pending job on the server, not just the stuck one, and those documents are gone; there is no recycle bin for spool files. Treat it accordingly.

Test first. Read the whole script before running it. Run it against a test print server or a lab VM with a dummy printer first, and confirm the filtering (the age cutoff, the printer name) behaves the way you expect before you point it at a busy production spooler. Start with -WhatIf so nothing is actually deleted until you've seen what would be.

No firewall/appliance changes here — this is a local Windows service, so there's nothing to lock yourself out of. The one caution: if this is a shared print server, run the spooler-reset variant in a quiet window, because it briefly takes all printing offline.

Assumptions

I'm writing for Windows Server 2019 or 2022 acting as a print server, with Windows PowerShell 5.1 (the built-in version) and the PrintManagement module, which ships in the box. The cmdlets — Get-Printer, Get-PrintJob, Remove-PrintJob — also exist on Windows 10/11, so you can adapt this for a workstation. The spool directory is the default C:\Windows\System32\spool\PRINTERS; if you've relocated it, substitute your path everywhere below.

Step 1: See what's actually queued

Before deleting anything, look. This lists every job across every local printer:

# List all queued jobs on this print server, newest first
Get-Printer | ForEach-Object {
    Get-PrintJob -PrinterName $_.Name -ErrorAction SilentlyContinue
} | Select-Object PrinterName, Id, DocumentName, JobStatus, SubmittedTime |
    Sort-Object SubmittedTime -Descending | Format-Table -AutoSize

JobStatus is the field to watch — values like Error, Deleting, or a job stuck in Spooling/Printing for hours are your candidates. SubmittedTime is how old it is.

Step 2: The gentle cleanup — cancel old jobs by age

Most stuck jobs will cancel cleanly through the print API. This is the version to reach for first, because it only touches jobs older than your cutoff and leaves the service running for everyone else.

# --- Cancel print jobs older than a set age, safely ---
$AgeMinutes = 30                                    # jobs older than this are targets
$cutoff     = (Get-Date).AddMinutes(-$AgeMinutes)

$targets = Get-Printer | ForEach-Object {
    Get-PrintJob -PrinterName $_.Name -ErrorAction SilentlyContinue
} | Where-Object { $_.SubmittedTime -lt $cutoff }

# Show what WOULD be removed. Nothing is deleted while -WhatIf is present.
$targets | Remove-PrintJob -WhatIf

Run that as-is first. -WhatIf reports each job it would remove without touching anything. When you're satisfied the list is right, remove -WhatIf to do it for real:

$targets | Remove-PrintJob

If you'd rather be prompted job-by-job, use -Confirm instead of -WhatIf.

A note on scope: SubmittedTime uses the server's clock. If you want to target a single problem queue instead of the whole server, replace the Get-Printer | ForEach-Object { … } block with a single call:

$targets = Get-PrintJob -PrinterName "Accounting-HP-01" |   # your exact printer name
           Where-Object { $_.SubmittedTime -lt $cutoff }

Replace Accounting-HP-01 with the real name from Get-Printer.

Step 3: The hard reset — when a job won't die

Sometimes Remove-PrintJob can't clear a job because the spool file itself is wedged. The long-established fix is to stop the spooler, delete the on-disk job files, and start it again. This clears all pending jobs on the server.

# --- Spooler reset: clears ALL queued jobs on this server ---
# Stop dependent services along with the spooler
Stop-Service -Name Spooler -Force

# Delete only the spool job files. .SPL = job data, .SHD = job metadata.
$spoolDir = "$env:SystemRoot\System32\spool\PRINTERS"
Get-ChildItem -Path $spoolDir -Include *.SPL, *.SHD -File -ErrorAction SilentlyContinue |
    Remove-Item -Force -Verbose

# Bring printing back online
Start-Service -Name Spooler

I've deliberately scoped the deletion to *.SPL and *.SHD inside the spool directory and used -File so it never wanders into subfolders or grabs anything else. Do not broaden that path or drop the -Include filter. Note that Get-ChildItem -Include needs the path to be enumerable — if you get no matches, confirm the extensions with Get-ChildItem $spoolDir first; some environments show the files without the -Include filter behaving as expected, in which case add -Recurse or check the exact path.

Step 4: Save it as a script

Put whichever version you want on a schedule into a .ps1. Here's the age-based cleanup as a standalone file:

# clear-old-printjobs.ps1
param(
    [int]$AgeMinutes = 30
)

$cutoff  = (Get-Date).AddMinutes(-$AgeMinutes)
$removed = 0

Get-Printer | ForEach-Object {
    Get-PrintJob -PrinterName $_.Name -ErrorAction SilentlyContinue
} | Where-Object { $_.SubmittedTime -lt $cutoff } | ForEach-Object {
    Remove-PrintJob -InputObject $_
    $removed++
    Write-Output "Removed job $($_.Id) '$($_.DocumentName)' on $($_.PrinterName)"
}

Write-Output "Done. Removed $removed job(s) older than $AgeMinutes minute(s)."

Save it somewhere like C:\Scripts\clear-old-printjobs.ps1 (substitute your own path). Test it by hand in an elevated prompt before scheduling:

powershell.exe -ExecutionPolicy Bypass -File "C:\Scripts\clear-old-printjobs.ps1"

Step 5: Schedule it

Use Task Scheduler — it's the standard, boring, reliable choice. Register a task that runs the script under an account with local admin rights, with Run with highest privileges ticked. You can create it from an elevated PowerShell prompt:

$action  = New-ScheduledTaskAction -Execute "powershell.exe" `
    -Argument '-ExecutionPolicy Bypass -File "C:\Scripts\clear-old-printjobs.ps1"'
$trigger = New-ScheduledTaskTrigger -Daily -At 6:00AM
$principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest

Register-ScheduledTask -TaskName "Clear old print jobs" `
    -Action $action -Trigger $trigger -Principal $principal

Running as SYSTEM avoids storing a password and has the rights needed on the local spooler. If your policy requires a named service account instead, use New-ScheduledTaskPrincipal with that account — check Microsoft Learn for Register-ScheduledTask and New-ScheduledTaskPrincipal for the exact parameter set.

Verify it worked

Re-run the listing from Step 1. The stuck jobs should be gone and the queue should show only current, legitimate work:

Get-Printer | ForEach-Object {
    Get-PrintJob -PrinterName $_.Name -ErrorAction SilentlyContinue
} | Format-Table PrinterName, Id, DocumentName, JobStatus -AutoSize

After a spooler reset, confirm the service came back up:

Get-Service -Name Spooler          # Status should read 'Running'

For the scheduled task, check it registered and ran:

Get-ScheduledTask -TaskName "Clear old print jobs"
Get-ScheduledTaskInfo -TaskName "Clear old print jobs"   # LastRunTime / LastTaskResult (0 = success)

Undoing it

There's no undo for a cancelled or purged print job — that's why the -WhatIf dry run in Step 2 matters. What is reversible is the schedule. To remove the task:

Unregister-ScheduledTask -TaskName "Clear old print jobs" -Confirm:$false

And if you ever stopped the spooler by hand and want it back exactly as it was, just start it again with Start-Service -Name Spooler; the service startup type isn't changed by anything above, so nothing else needs restoring.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.