Windows services are managed by the Service Control Manager, and sc.exe is the built-in client that talks to it directly. It reads and writes the same configuration that services.msc shows in a GUI: start type, binary path, logon account, dependencies and recovery actions – on the local machine or on a remote server.
Most admins only ever use sc query and sc stop. That leaves the useful half untouched. sc qc is the fastest way to see why the same service behaves differently on two servers, and sc failure is the only built-in way to script service recovery actions – PowerShell has no cmdlet for it even today.
This article covers the subcommands worth knowing, the exact spacing rule that makes half of all sc commands fail silently, and the parsing tricks that actually survive real output. Killing a service process that refuses to stop is covered separately in tasklist and taskkill.
Applies to: Windows 10 / 11, Windows Server 2016 / 2019 / 2022
Quick answer
The four commands that cover most day-to-day work. Run them from an elevated prompt – querying works unelevated, changing anything does not.
rem current state and PID of a single service
sc queryex Spooler
rem the real configuration - start type, binary, logon account, dependencies
sc qc Spooler
rem change start type - note the space after the equals sign, it is mandatory
sc config Spooler start= demand
rem stop and start
sc stop Spooler
sc start Spooler
start= demand works. start=demand prints the usage screen and changes nothing – it does not report an error.
What sc.exe does
sc.exe is a command-line client for the Service Control Manager. It does not edit the registry directly – it calls the same API that services.msc uses, so changes take effect immediately and are recorded the same way. Every subcommand accepts an optional server name in UNC form as the first argument, before the subcommand.
rem the server name goes BEFORE the subcommand, not after it
sc \\SRV-PROD-01 query Spooler
The subcommands that matter in practice:
| Subcommand | What it returns or changes | Needs elevation |
|---|---|---|
query | State of one service, or all active services | No |
queryex | Same as query plus PID and FLAGS | No |
qc | Configuration: start type, binary path, logon account, dependencies | No |
qfailure | Current recovery actions | No |
qdescription | The description text shown in services.msc | No |
getkeyname | Service key name from a display name | No |
enumdepend | Services that depend on this one | No |
config | Changes start type, binary path, account, dependencies | Yes |
failure | Sets recovery actions after a crash | Yes |
start / stop | Sends START / STOP control requests | Yes |
create / delete | Registers or removes a service | Yes |
sc query defaults to state= active. A stopped service is simply absent from the list until you add state= all. This is the single most common reason an enumeration looks incomplete.
Practical examples
1. The service name is not what services.msc shows you
The problem: The Services console lists “Print Spooler”, so you run sc stop "Print Spooler" and get [SC] OpenService FAILED 1060. The service exists, but SCM does not know it by that name.
The solution: sc getkeyname translates a display name into the key name every other subcommand expects. Quote the display name if it contains spaces – it is not case sensitive.
rem display name in, registry key name out
sc getkeyname "print spooler"
rem now the key name works everywhere else
sc query Spooler
[SC] GetServiceKeyName SUCCESS Name = Spooler
2. Same service, two servers, different behaviour
The problem: A vendor agent starts on SRV-PROD-01 and fails on SRV-PROD-02. Both show the same display name and the same version in services.msc.
The solution: sc qc prints the four fields that actually differ in practice – START_TYPE, BINARY_PATH_NAME, DEPENDENCIES and SERVICE_START_NAME. Run it against both servers and diff the output.
rem local
sc qc Spooler
rem remote - server name comes first, before the subcommand
sc \\SRV-PROD-01 qc Spooler
Expected output. SERVICE_START_NAME is the logon account – a service that runs as a domain account on one box and LocalSystem on another will behave differently no matter how identical the binary is.
[SC] QueryServiceConfig SUCCESS
SERVICE_NAME: Spooler
TYPE : 110 WIN32_OWN_PROCESS (interactive)
START_TYPE : 2 AUTO_START
ERROR_CONTROL : 1 NORMAL
BINARY_PATH_NAME : C:\Windows\System32\spoolsv.exe
LOAD_ORDER_GROUP : SpoolerGroup
TAG : 0
DISPLAY_NAME : Print Spooler
DEPENDENCIES : RPCSS
: http
SERVICE_START_NAME : LocalSystem
To pull a single field into a script, split on colon and space and take the remainder of the line. Splitting on colon alone breaks the path at the drive letter.
rem tokens=1* keeps everything after the first token, so C:\ survives intact
rem delims is a colon followed by a space - both are treated as separators
for /f "tokens=1* delims=: " %a in ('sc qc Spooler ^| findstr /c:"BINARY_PATH_NAME"') do @echo %b
C:\Windows\System32\spoolsv.exe – the full path, not truncated at the drive letter. Inside a batch file double the percent signs: %%a and %%b.
3. Change the start type without opening services.msc
The problem: A monitoring agent is set to start automatically on a test box and keeps generating alerts nobody wants. You need it off at boot but still startable by hand.
The solution: sc config writes the start type straight into SCM. The accepted values are boot, system, auto, demand, disabled and delayed-auto. “Manual” in the GUI is demand on the command line.
rem demand is what the GUI calls Manual - startable, but not at boot
sc config Spooler start= demand
rem delayed-auto starts after the auto-start services, which cuts boot contention
sc config Spooler start= delayed-auto
rem always read it back - config reports SUCCESS even when nothing useful changed
sc qc Spooler | findstr /c:"START_TYPE"
Put it back when the test window is over. Any article that changes a start type should show the way back.
rem restore the Windows default for Spooler
sc config Spooler start= auto
sc start Spooler
sc config changes configuration only. It does not stop or start anything. After start= disabled the service keeps running until the next stop or reboot, which is why “I disabled it” and “it is still running” are both true at the same time.
4. Make a flaky service restart itself
The problem: A vendor service dies roughly once a week at 03:00 and nobody notices until the morning. The GUI Recovery tab can fix it, but not across forty servers.
The solution: sc failure sets up to three actions, used on the first, second and third failure. Each action is followed by its delay in milliseconds and actions are separated by forward slashes. reset= is the error-free period in seconds after which the failure counter goes back to zero.
rem restart after 60s, restart again after 60s, then run a command on the third failure
rem reset= 86400 means the counter clears after a full day without a crash
sc failure Spooler reset= 86400 actions= restart/60000/restart/60000/run/1000
rem read the configuration back - this is the only way to confirm it took
sc qfailure Spooler
A command to run on the third failure is set separately with command=. For a batch file, call it through cmd.exe explicitly – SCM will not run a .bat on its own.
rem cmd.exe /c is required - SCM launches the binary, not the shell
sc failure Spooler reset= 86400 command= "cmd.exe /c C:\bat\notify-spooler.bat" actions= restart/60000/restart/60000/run/1000
Clearing the recovery actions again uses an empty actions= value.
rem back to "Take No Action" on every failure
sc failure Spooler reset= 0 actions= ""
reset= and actions= require each other. Sending one without the other fails. Also note that recovery actions only fire when the service terminates unexpectedly – a clean sc stop is not a failure and triggers nothing.
5. Know what falls over before you stop it
The problem: You need to restart a service on a production server during a short window. Stopping it takes down two other services that depend on it, and you find out from the ticket queue.
The solution: sc enumdepend lists everything that cannot run without the target service. Run it first, then use sc queryex to confirm the process actually went away.
rem what breaks if this service stops
sc enumdepend Spooler
rem 1024-byte default buffer is often too small on core services
rem the error message tells you the exact size to pass as the last argument
sc enumdepend rpcss 6971
Then confirm the state and the PID. queryex adds the PID and FLAGS lines that plain query leaves out.
SERVICE_NAME: Spooler
TYPE : 110 WIN32_OWN_PROCESS (interactive)
STATE : 4 RUNNING
(STOPPABLE, PAUSABLE, ACCEPTS_SHUTDOWN)
WIN32_EXIT_CODE : 0 (0x0)
SERVICE_EXIT_CODE : 0 (0x0)
CHECKPOINT : 0x0
WAIT_HINT : 0x0
PID : 3184
FLAGS :
To capture just the PID for a follow-up command, the PID line has exactly three whitespace-separated tokens – the label, the colon and the number.
rem tokens=3 lands on the number because the colon is its own token
for /f "tokens=3" %a in ('sc queryex Spooler ^| findstr /c:"PID"') do @echo %a
0 means the service is not running. If the process refuses to die after sc stop, see tasklist and taskkill – but stop through SCM first, or the Service Control Manager keeps reporting the service as running.
Hidden gems
findstr treats spaces as OR, not as a phrase
This is the single biggest source of wrong output when filtering sc results. findstr "SERVICE_NAME STOPPED" matches any line containing either word, so a filter meant to find stopped services returns every service name as well. Use /c: for a literal string.
rem WRONG - matches every SERVICE_NAME line and every STOPPED line
sc query state= all | findstr "SERVICE_NAME STOPPED"
rem RIGHT - /c: makes the whole argument one literal search string
sc query state= all | findstr /c:"1 STOPPED"
Never split BINARY_PATH_NAME on a colon alone
for /f "tokens=2 delims=:" looks like the obvious way to grab the value after the label. On the binary path it returns C – the drive letter’s own colon ends the token. Use tokens=1* delims=": " as shown in example 2, or move to PowerShell for anything more than one field.
sc delete only marks a running service for deletion
If the service is running, or any process still holds an open handle to it, sc delete returns SUCCESS but the service stays in SCM and in services.msc until every handle closes – frequently until reboot. Stop the service first, then close services.msc, then delete.
sc query sees drivers that services.msc never shows
The Services console only lists Win32 services. type= driver enumerates kernel and file-system drivers from the same SCM database, which is the fastest way to confirm whether a filter driver is loaded.
rem kernel and file system drivers - invisible in services.msc
sc query type= driver
rem drivers in a specific load-order group
sc query type= driver group= NDIS
type= can legitimately appear twice in one sc query. The first occurrence selects what to enumerate (driver, service, all), the second selects the service or driver type (own, share, interact, kernel, filesys and so on). It is not a typo in the documentation.
PowerShell equivalents
PowerShell covers querying and start types cleanly. It does not cover recovery actions at all – there is still no built-in cmdlet for what sc failure does.
The direct replacement for sc qc. Since PowerShell 6 the ServiceController object carries BinaryPathName, UserName, StartupType and DelayedAutoStart, so one line replaces reading the whole sc qc block.
# StartType alone reports Automatic for delayed-auto services -
# DelayedAutoStart is the property that tells them apart
Get-Service -Name Spooler |
Select-Object Name, DisplayName, Status, StartType, DelayedAutoStart, UserName, BinaryPathName
The equivalent of sc config start=. Set-Service accepts AutomaticDelayedStart directly, which is cleaner than the delayed-auto spelling in CMD.
# Manual in the GUI, demand in sc.exe, Manual again in PowerShell
Set-Service -Name Spooler -StartupType Manual
# read it back
Get-Service Spooler | Select-Object Name, StartType, Status
For the PID and the exact command line, CIM is more reliable than parsing sc queryex output. Win32_Service.PathName includes any arguments the binary was registered with, which BINARY_PATH_NAME also shows but is harder to split.
# StartMode returns Auto for both plain and delayed automatic services -
# check DelayedAutoStart separately if the distinction matters
Get-CimInstance Win32_Service -Filter "Name='Spooler'" |
Select-Object Name, State, StartMode, DelayedAutoStart, ProcessId, StartName, PathName
Get-Service -ComputerName was removed in PowerShell 6 and is not coming back. For remote queries either use Invoke-Command -ComputerName SRV-PROD-01 { Get-Service Spooler }, or stay with sc \\SRV-PROD-01 query Spooler, which still works from any CMD prompt with no remoting configured.
Where this matters
- Post-patch validation – after a maintenance window,
sc \\SRV-PROD-01 queryacross a server list confirms in seconds which services did not come back up. - Server build drift –
sc qcagainst a known-good reference server exposes a wrong logon account or a changed binary path faster than any GUI comparison. - Unattended recovery –
sc failureacross a fleet turns a nightly crash into a self-healing restart instead of a morning incident. - Change windows –
sc enumdependbefore a restart turns an unplanned outage into a planned one with the dependent services listed in the change ticket. - Security review –
sc qcshows every service still running under a domain account instead of a managed service account, which is the first thing an auditor asks for. - Server Core and rescue prompts – there is no services.msc on Server Core and none in a recovery console.
sc.exeis what remains.
Tips and limitations
- Querying needs no elevation.
config,failure,start,stop,createanddeleteall do – without it you get[SC] OpenService FAILED 5: Access is denied. - In PowerShell,
scis an alias forSet-Content. Always typesc.exein a PowerShell window, or you will silently create a file instead of touching a service. - Error 1060 is “service does not exist” – usually a display name used where a key name was required. Error 1072 means the service is already marked for deletion.
sc stopreturns as soon as the STOP request is accepted, not when the service has stopped. Pollsc queryif a script depends on the service actually being down.- Not every service accepts changes to its failure options – services that run as part of a shared service set often refuse.
- Remote use goes over RPC on the SMB named pipe, so it needs TCP 445 reachable and local admin rights on the target. It does not need WinRM, which is why
sc \\SRV-PROD-01still works on hosts where PowerShell remoting was never enabled. - Do not use
sc deleteon built-in roles and features such as DHCP, DNS or IIS. Remove them through Server Manager instead.
Official documentation
- sc.exe query – Windows Commands | Microsoft Learn
- sc.exe config – Windows Commands | Microsoft Learn
- sc.exe create – Windows Commands | Microsoft Learn
- Set-Service – PowerShell | Microsoft Learn
Related tools
- Event Log Analyzer – service crashes and recovery actions are logged as System events 7000, 7009, 7031 and 7034; paste the log and read them without opening Event Viewer.
- Port Checker – when
sc querysays RUNNING but nothing answers, confirm whether the service is actually listening on its port.
Related guides
- schtasks in Windows: create, query and audit scheduled tasks from CMD: the command line side of scheduled tasks, from audit to XML deployment.
- tasklist and taskkill in Windows – the next step when a service process ignores
sc stopand has to be ended by PID. - Get-WinEvent for Windows event logs – filtering the System log for the Service Control Manager events that record every start, stop and unexpected termination.
- Troubleshooting high CPU with Performance Monitor – once
sc queryexgives you the PID, Performance Monitor tells you what that process is doing. - Windows command line CMD cheat sheet – the wider CMD reference, including the FOR /F tokenising used in the examples above.