Every PowerShell list on the internet is sorted alphabetically. That's fine when you already know the name of the command you want, and no help at 4pm with a user on the phone whose laptop sounds like a hairdryer. This one is sorted by the ticket instead: the machine is slow, the disk is full, the service died, the share won't mount. Each command is one line you can paste into a console, with what it hands back and where it bites. Version differences between Windows PowerShell 5.1 and PowerShell 7 are flagged where they'll catch you.
TL;DR
- Start with three.
Get-Help,Get-CommandandGet-Memberwill teach you every other command on this page. - Daily triage.
Get-Process,Get-VolumeandGet-WinEventcover the slow-machine, full-disk and dead-service tickets. - Network.
Test-NetConnection <host> -Port <n>replaces ping plus telnet, with no UDP support. - At scale.
Invoke-Commandruns any of it across many machines at once, if WinRM is on. - Before you change anything. Run the
Get-version first, then add-WhatIf.
Every One-Liner in One Table
If you came looking for a PowerShell commands cheat sheet, this table is it. The sections after it explain each line and where it bites.
| What you need | The command | The catch |
|---|---|---|
| Top CPU processes | Get-Process | Sort CPU -Desc | Select -First 10 | CPU is cumulative seconds, not percent |
| Real-time CPU % | (Get-Counter '\Process(*)\% Processor Time').CounterSamples | Counter names are localized |
| Top memory | Get-Process | Sort WS -Desc | Select -First 10 | Working set over-sums shared pages |
| Uptime | (Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime | Get-Uptime is PS 6+ only |
| Startup items | Get-CimInstance Win32_StartupCommand | Misses scheduled tasks |
| Free space | Get-Volume | Where DriveLetter | Windows-only module |
| Biggest files | Get-ChildItem C:\ -Recurse -File | Sort Length -Desc | Minutes on a big volume |
| Clear temp | Get-ChildItem $env:TEMP -Recurse | Remove-Item -Recurse -Force | $env:TEMP is per-user |
| Stopped auto services | Get-CimInstance Win32_Service -Filter "StartMode='Auto' AND State='Stopped'" | Auto in CIM, Automatic in .NET |
| Restart a service | Restart-Service -Name Spooler -Force | -Force means "don't confirm" |
| Process behind a service | Get-CimInstance Win32_Service | Select Name, ProcessId | Many services share one PID |
| Unexpected shutdowns | Get-WinEvent -FilterHashtable @{LogName='System'; Id=6008,41,1074} | Filter in the hashtable, not after |
| Service crashes | Get-WinEvent -FilterHashtable @{LogName='System'; Id=7034} | Get-EventLog is gone in PS 6+ |
| Failed logons | Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625} | Needs elevation or Event Log Readers |
| Port test | tnc <host> -p <port> | No UDP |
| IP config | Get-NetIPConfiguration -All | Default hides disconnected adapters |
| Flush DNS | Clear-DnsClientCache | All or nothing |
| Listening ports | Get-NetTCPConnection -State Listen | It's Listen, not Listening |
| Local admins | Get-LocalGroupMember -Group 'Administrators' | Breaks on unresolvable SIDs |
| Installed software | Get-ItemProperty 'HKLM:\...\Uninstall\*' | Never use Win32_Product |
| Patches | Get-HotFix | Sort InstalledOn -Desc | CBS updates only |
| Run it everywhere | Invoke-Command -ComputerName $list -ScriptBlock {...} | WinRM is off on client Windows |
Triage a Machine That's Crawling
The first thing to know about Get-Process is that its CPU column answers a different question than the one you're asking.
powershellGet-Process | Sort-Object CPU -Descending | Select-Object -First 10 Name, Id, CPU, WS
Microsoft defines it as "the amount of processor time that the process has used on all processors, in seconds." Cumulative, since the process started. A Chrome window open since Monday outranks whatever is pinning a core right now: useful for finding a long-running hog, misleading for "why is this slow now."
For the instantaneous number, you want performance counters:
powershell(Get-Counter '\Process(*)\% Processor Time').CounterSamples | Sort-Object CookedValue -Descending | Select-Object -First 10 InstanceName, CookedValue
Filter out _Total and Idle, which sort straight to the top, and expect values over 100% on a multi-core box since they sum across cores. Two catches: counter names are localized, so this breaks on non-English Windows unless you look up local names with Get-Counter -ListSet, and Get-Counter was missing from 6.x entirely.
Memory, same shape:
powershellGet-Process | Sort-Object WS -Descending | Select-Object -First 10 Name, Id, @{n='WS(MB)';e={[int]($_.WS/1MB)}}
Working set isn't committed memory, and shared pages count against every process touching them, so the column over-sums. It still points at the right process.
Two more that close tickets on their own:
powershell(Get-Date) - (Get-CimInstance Win32_OperatingSystem).LastBootUpTime Get-CimInstance Win32_StartupCommand | Select-Object Name, Command, Location, User
The first verifies "have you tried turning it off and on again" rather than asking. Get-Uptime is tidier but arrived in PowerShell 6, so the CIM version works everywhere. The second covers Run and RunOnce keys plus both Startup folders, but not scheduled tasks, where the genuinely annoying stuff hides now.
Find Out Where the Disk Went
powershellGet-Volume | Where-Object DriveLetter | Select-Object DriveLetter, FileSystemLabel, @{n='FreeGB';e={[math]::Round($_.SizeRemaining/1GB,1)}}, @{n='SizeGB';e={[math]::Round($_.Size/1GB,1)}}
The Where-Object drops recovery and EFI partitions, which have no drive letter. Then find the weight:
powershellGet-ChildItem C:\ -Recurse -File -ErrorAction SilentlyContinue | Sort-Object Length -Descending | Select-Object -First 20 FullName, Length
Length is bytes. -File matters more than it looks, because Length on a directory object throws. Expect minutes on a large volume with no output until it finishes: Sort-Object buffers every object in memory before returning the first one.
Per-folder totals, usually what you want on a user profile:
powershellGet-ChildItem C:\Users -Directory | ForEach-Object { [pscustomobject]@{ Path = $_.FullName SizeGB = [math]::Round((Get-ChildItem $_.FullName -Recurse -File -ErrorAction SilentlyContinue | Measure-Object Length -Sum).Sum/1GB, 2) } } | Sort-Object SizeGB -Descending
One 5.1 difference: from PowerShell 7.3, Measure-Object stopped erroring on objects missing the property you asked for. On 5.1 it throws, so that inner -ErrorAction SilentlyContinue is load-bearing.
Clearing temp:
powershellGet-ChildItem $env:TEMP -Recurse | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue
Written that way for a documented reason: Remove-Item's own -Recurse has a known issue, and Microsoft's docs work around it with exactly this pipe. The trap catches everyone once: $env:TEMP is per-user, so running it elevated cleans the admin's temp folder and leaves the user's untouched.
Work Out Why a Service Stopped
Start with the trap, because it returns zero rows and no error, and sends you hunting a problem that was never there. WMI's Win32_Service.StartMode calls auto-start Auto. The .NET enum behind Get-Service calls it Automatic. Match the wrong word to the wrong source and you get a clean, empty, completely wrong answer.
powershell# PowerShell 7 Get-Service | Where-Object { $_.StartType -eq 'Automatic' -and $_.Status -eq 'Stopped' } # works everywhere, and faster Get-CimInstance Win32_Service -Filter "StartMode='Auto' AND State='Stopped'" | Select-Object Name, DisplayName, StartMode, State
The CIM version filters server-side instead of pulling back every service and discarding most of them. Before you fix what it returns: delayed-start services show Stopped for a while after boot, and trigger-start services sit Stopped by design. Neither is broken.
powershellRestart-Service -Name Spooler -Force
-Force here means "don't ask me to confirm," not "handle dependent services." That behaviour belongs to Stop-Service -Force. If something has running dependents, stop it with Stop-Service -Force and start it again as two steps.
And the join that explains why killing one process took half the machine with it:
powershellGet-CimInstance Win32_Service -Filter "State='Running'" | Where-Object ProcessId | Select-Object Name, ProcessId, @{n='Process';e={(Get-Process -Id $_.ProcessId -ErrorAction SilentlyContinue).Name}}
ProcessId is 0 for anything stopped. Many services share one host process, so this is many-to-one by design: Win32_Service's ServiceType tells you which is which, with Own Process and Share Process as the values that matter.
Read the Event Log Without Drowning In It
Get-EventLog is gone: dropped in PowerShell 6, absent from every 7.x build, because it leaned on unsupported APIs. Microsoft discourages it even on 5.1, where its own docs call the underlying Win32 API deprecated and warn "the results may not be accurate." Learn Get-WinEvent instead.
powershellGet-WinEvent -FilterHashtable @{LogName='System'; Id=6008,41,1074; StartTime=(Get-Date).AddDays(-7)}
The unexpected-shutdown trio: 6008 (dirty shutdown), 41 (kernel power, lost power without a clean stop), 1074 (something asked for the restart, and it names what).
Why the hashtable rather than piping into Where-Object? The docs are blunt: filters "are applied as the objects are retrieved," while Where-Object "retrieves all of the objects, then applies filters to all of the objects." On a Security log with half a million entries that's two seconds versus a coffee break.
powershellGet-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Service Control Manager'; Id=7034} -MaxEvents 20 Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddDays(-1)} -MaxEvents 50
7034 is a service terminating unexpectedly. 4625 is a failed logon, and it turns "my account keeps locking out" into an answer. Read the status code: 0xC000006A bad password, 0xC0000064 bad username (a run of those is someone guessing at account names), 0xC0000072 disabled, 0xC0000234 already locked. Logon type 3 is network, type 10 is RDP.
The Security log needs elevation or membership in Event Log Readers, and here's the nasty part: a permissions failure returns the same "no events found" message as a genuinely empty result. An unelevated query looks exactly like good news.
Test the Network Path in One Line
Test-NetConnection was the highest-voted answer in the r/sysadmin thread that still ranks on page one for this topic, and the reply underneath it explained why: "It's an essential command that surprisingly few people seem to know."
powershelltnc mail.contoso.com -p 443 (Test-NetConnection 10.0.0.5 -Port 3389).TcpTestSucceeded
tnc is the alias, -ComputerName is positional so you can drop it, and -p works for -Port. The second form returns a bare True or False for scripts.
The limitation to know before you lose an hour: no UDP. For that you're back to netcat or a capture.
powershellGet-NetIPConfiguration -All Clear-DnsClientCache Resolve-DnsName mail.contoso.com -Type MX -Server 8.8.8.8 Get-NetTCPConnection -State Listen | Select-Object LocalAddress, LocalPort, OwningProcess, @{n='Process';e={(Get-Process -Id $_.OwningProcess).ProcessName}} | Sort-Object LocalPort
-All matters on anything running Hyper-V or a VPN client, because the default shows connected physical adapters only and hides the one causing the problem. Clear-DnsClientCache is exactly ipconfig /flushdns.
The last one replaces netstat -anb. The state is Listen, not Listening, which everyone gets wrong once. netstat -b does the same job, but Microsoft warns it "can be time-consuming and will fail unless you have sufficient permissions." The PowerShell version pulls process names from unprivileged data and hands back sortable objects.
Check Users, Sessions and Local Admins
powershellGet-LocalGroupMember -Group 'Administrators' | Select-Object Name, ObjectClass, PrincipalSource quser
PrincipalSource is the column that earns its place: it tells you whether each admin is Local, Active Directory, a Microsoft Entra group or a Microsoft Account. That's the answer to "who can elevate on this box."
Recognize this rather than debugging it: Failed to compare two elements in the array. It fires when the group holds a SID that won't resolve, usually an Entra SID, an Intune enrollment account, or a machine moved out of a domain. It hits 5.1 as well as 7, and Microsoft closed the tracking issues without a fix. Fall back to net localgroup Administrators.
On PowerShell 7 the LocalAccounts module runs only through the compatibility shim, which hands back serialized copies rather than live objects, so methods on the returned user objects won't fire. That's behind the "works in 5.1, breaks in 7" reports on this module.
See What's Installed and What's Patched
Start with the one to never run:
powershellGet-WmiObject Win32_Product # don't
Microsoft's own class documentation explains why, and it's worse than slow: querying it "initiates a consistency check of packages installed, verifying and repairing the install." Every MSI application on the machine gets reconfigured, event 1035 firing once per app, plus 11708 failures under a non-admin account. There's a Microsoft KB called Windows Installer reconfigured all applications that exists because of this. It's incomplete too, since it only sees MSI-installed software.
The registry answers the same question without touching anything:
powershellGet-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*', 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' -ErrorAction SilentlyContinue | Where-Object DisplayName | Select-Object DisplayName, DisplayVersion, Publisher, UninstallString
Three paths, because 32-bit apps on 64-bit Windows land in WOW6432Node and per-user installs land in HKCU. Where-Object DisplayName drops patch entries and orphans. Don't trust InstallDate: Microsoft documents it as "the last time this product received service," so every patch rewrites it, and plenty of non-MSI installers never write it at all.
powershellGet-HotFix | Sort-Object InstalledOn -Descending | Select-Object -First 10 winget list --upgrade-available winget upgrade --all --include-unknown
Get-HotFix is narrower than it looks: the docs are explicit that it returns only Component Based Servicing updates, and that "updates supplied by Microsoft Windows Installer (MSI) or the Windows Update site aren't returned." On Windows 10 and 11 cumulative updates supersede each other, so a short list is normal and it is not a patch history.
--include-unknown isn't optional in practice, because anything whose installed version winget can't read gets skipped silently without it. Microsoft also publishes Microsoft.WinGet.Client, stable at 1.29.280 as of July 2026, which returns objects instead of text: Get-WinGetPackage | Where-Object IsUpdateAvailable. PSWindowsUpdate, the third-party module everyone reaches for, is still at 2.2.1.5 from July 2024, isn't Microsoft-supported, and is blocked from remote sessions by a Windows Update Agent restriction.
The Three Commands That Teach You the Rest
Thotaz, in that same r/sysadmin thread, pushed back on this entire genre of post: "Don't try to learn random oneliners... Instead of that you should put in the effort to learn the basic syntax and mechanics of PowerShell." Fair enough. Here's the short path.
powershellGet-Command -Noun *Service* # what exists Get-Help Get-Service -Examples # how it's used Get-Service | Get-Member # what came back, and what it can do
Get-Member is the one experienced people kept naming, with aMazingMikey putting it plainly: "If you want to really understand PowerShell, Get-Member. Pipe to it." Pipe into it rather than using -InputObject: piping a collection gets you the members of each object, -InputObject gets you the members of the collection.
Two notes. -Verb and -Noun can't be combined with -Name, so it's Get-Command -Noun *Service* or Get-Command -Name *Service* -CommandType Cmdlet, never both. And if Get-Help returns nothing but syntax, run Update-Help: Windows ships without help files, which needs elevation on 5.1 but defaults to CurrentUser scope from PowerShell 6.1.
Mistakes Worth Skipping
These aren't style preferences. Each is encoded as a rule in PSScriptAnalyzer, Microsoft's own linter.
Format-Table ends the pipeline. Anything downstream gets nothing usable. The mechanism is documented rather than folklore: Format-Table returns format objects of type Microsoft.PowerShell.Commands.Internal.Format, so past that point you're piping rendering instructions instead of your data. Format last, always.
Aliases belong in the console, not in scripts. The cleanest statement of this came out of the r/sysadmin thread and deserves to be the rule everywhere: "Gci goes in the blue window. Get-childitem goes in the white window." Type gci interactively all you like. In something another tech inherits, write it out.
-ErrorAction SilentlyContinue on everything. jeroen-79 described the pattern as an infomercial: "Senior techs hate him! See how this junior gets work done without errors by simply adding '-ErrorAction SilentlyContinue' to all his commands." Use it where you expect and understand the noise, like a recursive walk hitting folders you can't read. Don't use it to make red text go away. Suppressed errors still land in $Error, so you can check afterwards.
Skipping the Get- first. LetMeAskPls put the discipline in one line: always do a GET before you do a SET. Read the current value, change it, read it back to confirm it landed where you expected. Two extra commands, and it's the difference between a change and a guess.
Run It Across the Fleet, Not One Machine at a Time
Everything above assumes one machine. The job usually isn't.
powershellInvoke-Command -ComputerName (Get-Content .\servers.txt) -ThrottleLimit 32 -ScriptBlock { $PSVersionTable.PSVersion }
Thirty-two machines at a time by default, and it carries the best war story in the thread. ArmedwWings, during the CrowdStrike outage: "I was able to use invoke command to delete the trouble file in the 3-5 seconds the computers were up before crashing." Milkshakes00 did the same with a ping loop: "when it returned a connection I started blasting it with remove-item."
For repeated work against the same boxes, hold a session and pass local variables in with $using::
powershell$s = New-PSSession -ComputerName SRV01, SRV02 -Credential (Get-Credential) Invoke-Command -Session $s -ScriptBlock { Get-WinEvent -LogName $using:Log -MaxEvents 10 } Remove-PSSession $s
Remove them when you're done, because sessions hold server-side resources against a WinRM quota that defaults to five shells per user.
Four things bite at scale.
Remoting is off on client Windows. On by default from Server 2012, disabled on every workstation. Enable-PSRemoting fails on public networks unless you add -SkipNetworkProfileCheck, and even then it only opens the local subnet. StaticVoidMain2018's reply on r/sysadmin: "Never been in an org where psremoting is enabled."
Version drift. 5.1 is the only version guaranteed present, which is why calculatetech, running Datto and Panda, told r/msp that "everything must be written in PowerShell 2.0 for Windows or you'll hit compatibility issues." On PowerShell 7, Enable-PSRemoting creates PowerShell.7 endpoints while default remoting still lands on the 5.1 one unless you name the configuration.
SYSTEM context. RMM script engines mostly run as SYSTEM, which BuldozzerMSP flagged on r/msp: "my RMM (Datto) usual runs scripts as the system user." That breaks the HKCU half of the software inventory above, and winget too, since it registers per user.
-WhatIf isn't everywhere. Each cmdlet has to opt in, so check with (Get-Command Restart-Service).Parameters.Keys -contains 'WhatIf'. brokerceej's answer on r/msp is worth stealing wholesale: build your own. "Every script I write has a -DryRun parameter and comprehensive transcript based output to a logfile." Acronis Threat Research Unit, looking at 100 million script runs across 11,500 organizations in May 2026, found 97% of scripts take no parameters at all and 85% get abandoned after being written. A one-liner hardcoded to one machine is one nobody reuses.
At some point the console stops being the right place for this. Running a one-liner against four hundred endpoints needs an inventory of what's out there, a way to reach machines that are asleep or off the network, and a record of what ran where and what came back. That's what a script engine inside an RMM is for.
OpenFrame is ours, and what's worth knowing here is the shape of it rather than a feature list: open source, $1 per device per month, with RMM, patching, remote access, documentation and SIEM in one place instead of four contracts and four agents on every endpoint. The AI agents in it, Fae and Mingo, do ticket work inline rather than suggesting it to a tech, so the routine diagnostics and remediation clear themselves and your team gets those hours back for the work that needs a human. The one-liners on this page are the kind of thing they run.
Where to Go Next
Keep this open as a PowerShell commands cheat sheet if that's useful, but the actual upgrade is Get-Command, Get-Help -Examples and Get-Member. Those three turn any unfamiliar module into something you can work out alone, which no list can do for you.
Two things worth doing this week. Get a -DryRun switch into every script you write before it goes anywhere near a client, because -WhatIf won't be there when you need it most. And if you're running these one machine at a time across client sites, that's the signal to move the work into a script engine.
Our breakdown of IT automation tools picks up where the console stops.
Conrad Lunderstedt
Solution Architect
I'm Conrad, Solution Architect at Flamingo. Before this I spent the better part of twenty years in IT and managed services, a lot of it sitting next to technicians while they tried to make software do what the brochure said it would. Now I spend my days talking with MSPs about the stack they already run, and helping them work through the requests and issues that come with it.
