Windows Service Startup and Recovery
Windows Service Startup and Recovery
When a Windows service fails, identify the stage: account logon, connection to SCM, startup processing, or termination after it ran. Error 1053, error 1069, and recovery actions address different stages; increasing a timeout cannot solve all three.
Start with service logon and dispatcher-entry checks on Windows Server 2022, compare connection versus initialization on Windows 10 LTSC 2021, then inspect crash recovery on Server 2022. Each example uses a dedicated service; identify the failing stage before changing a business service.
1. Errors and logs
| Symptom | First evidence | Unsupported shortcut |
|---|---|---|
| 1069, logon failure | StartName, stored credential, allow/deny rights, 4625 | Always a wrong password |
| 1053, response timeout | Entry point, dispatcher connection, state reporting, stage logs | No code executed, or all initialization has a fixed 30-second limit |
| 1067, unexpected termination | Exceptions, exit codes, dumps, initialization logs | Every case failed at the same initialization stage |
| 1068, dependency failure | Configured dependencies and their states | Reset this service's password first |
| No restart after a crash | Failure actions, actual exit path, flag, 7031/7034 | Three restart entries impose a three-retry ceiling |
Use sc.exe explicitly in administrator Windows PowerShell 5.1; sc can alias Set-Content. Capture the internal service name and configuration:
$n = Read-Host 'Service name (not display name)'
# This example accepts simple internal names to keep the WMI filter exact.
if ($n -notmatch '^[A-Za-z0-9_.-]+$') { throw 'Use a confirmed simple service name.' }
Get-CimInstance Win32_Service -Filter "Name='$n'" |
Select-Object Name,State,ProcessId,PathName,StartName,StartMode,ExitCode
sc.exe qc $n
sc.exe qfailure $n
sc.exe qfailureflag $n
$since = (Get-Date).AddMinutes(-15)
Get-WinEvent -FilterHashtable @{
LogName='System';ProviderName='Service Control Manager';StartTime=$since
} | Where-Object { $_.ToXml().Contains($n) } |
Select-Object TimeCreated,Id,RecordId,MessageCorrelate time, service, and record ID for the same attempt. Events guide investigation but do not replace process/application evidence. A 7009 reports waiting for connection; it does not make application logs irrelevant.
2. Service logon 1069
NAD0720261009Svc used an ordinary local account on Server 2022. The program and directory permissions remained fixed while the stored password and then SeServiceLogonRight were changed:
| Condition | Start | SCM | 4625 Status / SubStatus |
|---|---|---|---|
| Correct password, allowed service logon | Success | Normal transitions | No corresponding failure |
| Wrong stored service password | 1069 | 7038, 7000 | 0xc000006d / 0xc000006a |
| Correct password restored | Success | Normal transitions | No corresponding failure |
| Correct password, user's logon right removed | 1069 | 7041, 7000 | 0xc000015b / 0x0 |
| Exact right restored | Success | Normal transitions | No corresponding failure |
The failed logons were Type 5 from services.exe. The first substatus identified a password mismatch; the second identified an ungranted logon type. Neither reached application work. See 4625 fields/types.
Read named fields for the precise account:
$account='NAD0720261009User'
Get-WinEvent -FilterHashtable @{
LogName='Security'
Id=4625
StartTime=(Get-Date).AddMinutes(-10)
} | ForEach-Object {
$xml=[xml]$_.ToXml()
$f=@{}
foreach($d in $xml.Event.EventData.Data){$f[$d.Name]=$d.'#text'}
if($f.TargetUserName -eq $account -and $f.LogonType -eq '5'){
[pscustomobject]@{
TimeCreated=$_.TimeCreated
RecordId=$_.RecordId
TargetUserName=$f.TargetUserName
TargetDomainName=$f.TargetDomainName
Status=$f.Status
SubStatus=$f.SubStatus
LogonType=$f.LogonType
ProcessName=$f.ProcessName
}
}
}For the password branch, update this service's stored credential in services.msc, or use an interactive credential with CIM Change. This changes the service configuration, not the account's password again:
$name='NAD0720261009Svc'
$svc=Get-CimInstance Win32_Service -Filter "Name='$name'"
$cred=Get-Credential -UserName $svc.StartName -Message 'Enter current service account password'
$result=Invoke-CimMethod -InputObject $svc -MethodName Change -Arguments @{
StartPassword=$cred.GetNetworkCredential().Password
}
if($result.ReturnValue -ne 0){throw "Service Change failed: $($result.ReturnValue)"}
Start-Service $name
(Get-Service $name).WaitForStatus('Running',[TimeSpan]::FromSeconds(15))
Get-CimInstance Win32_Service -Filter "Name='$name'" |
Select-Object Name,State,StartName,ProcessIdThe interface obtains plaintext in memory for local SCM; avoiding command history does not mean the secret never exists in process memory. Confirm the existing service identity, check Change=0, then actually start and validate. Do not bypass 1069 by switching to LocalSystem.
For the rights branch, export and inspect both lists, preserving unrelated entries:
Get-LocalUser -Name 'NAD0720261009User' |
Select-Object Name,Enabled,PasswordExpires,PasswordLastSet,SID
secedit.exe /export /cfg "$env:ProgramData\service-rights-current.inf" /areas USER_RIGHTS
Select-String -Path "$env:ProgramData\service-rights-current.inf" -Pattern '^Se(ServiceLogonRight|DenyServiceLogonRight)\s*='Deny service logon wins, including a deny inherited through group membership. If domain policy manages the right, fix its scope for the service's computer; local changes may be overwritten. Verify through the next service logon without rebooting the entire host solely for this right. See Log on as a service.
After repair, the same ordinary account advanced output from 08:34:44.4437852Z to 08:34:46.4597501Z. Validate Running together with fresh output, not a file left from before the change.
3. Service entry 1053
A separate Server 2022 control used one ContractProbe.exe with console, ordinary, and service branches. console wrote and exited; ordinary wrote and waited 90 seconds without connecting to SCM; service used ServiceBase.Run with OnStart/OnStop.
| Entry point | Result |
|---|---|
| Manual console | Exit zero, file written |
| Registered service | Running, advancing heartbeat, clean stop |
| Same EXE changed to ordinary | 1053; 7009/7000; ultimately Stopped |
| Restored service branch | Recovered under the same identity/ACL |
The failing branch wrote at 07:38:40.5321141Z. At 07:39:10.5321847Z, 7009 reported a 30000-ms connection wait. Code executed and file access succeeded; the missing component was the service entry contract. 1053 does not prove no code ran.
SCM requires timely connection to its dispatcher. New-Service/sc create stores configuration rather than injecting a service protocol into an arbitrary EXE. See StartServiceCtrlDispatcher.
This retained minimal reproduction uses a dedicated LocalSystem lab service writing only its own directory; that identity is not a business-service recommendation. The reviewed setup uses sc.exe for configuration and refuses existing names/directories:
$ErrorActionPreference='Stop'
$r='C:\NAD0520261009'
$n='NAD0520261009'
if ((Test-Path $r) -or (Get-Service $n -ErrorAction SilentlyContinue)) {
throw 'Test name or directory already exists'
}
New-Item -ItemType Directory $r | Out-Null
$src=@'
using System;
using System.IO;
using System.ServiceProcess;
using System.Threading;
public class ContractProbe : ServiceBase {
static string root = @"C:\NAD0520261009";
Timer timer;
static void Work() { File.WriteAllText(Path.Combine(root,"heartbeat.txt"),DateTime.UtcNow.ToString("o")); }
public ContractProbe() { ServiceName="NAD0520261009"; CanStop=true; }
protected override void OnStart(string[] args) { Work(); timer=new Timer(s=>Work(),null,1000,1000); }
protected override void OnStop() { timer.Dispose(); File.WriteAllText(Path.Combine(root,"stopped.txt"),"stopped"); }
public static void Main(string[] args) {
if(args.Length>0 && args[0]=="service") Run(new ContractProbe());
else { Work(); if(args.Length>0 && args[0]=="console") return; Thread.Sleep(90000); }
}
}
'@
Add-Type -TypeDefinition $src -ReferencedAssemblies System.ServiceProcess.dll `
-OutputAssembly "$r\ContractProbe.exe" -OutputType ConsoleApplication
if (-not (Test-Path "$r\ContractProbe.exe")) { throw 'Compilation failed' }
$p=Start-Process "$r\ContractProbe.exe" -ArgumentList console -PassThru -Wait
$p.ExitCode
Get-Content "$r\heartbeat.txt"
New-Service -Name $n -BinaryPathName '"C:\NAD0520261009\ContractProbe.exe" service' `
-StartupType Manual | Out-Null
Start-Service $n
(Get-Service $n).WaitForStatus('Running',[TimeSpan]::FromSeconds(10))
Stop-Service $n
sc.exe config $n binPath= '"C:\NAD0520261009\ContractProbe.exe" ordinary'
if ($LASTEXITCODE -ne 0) { throw "Service configuration failed" }
sc.exe start $nThe last command intentionally times out. After confirming the failed process has exited, restore the entry the program actually implements:
$n = 'NAD0520261009'
sc.exe config $n binPath= '"C:\NAD0520261009\ContractProbe.exe" service'
if ($LASTEXITCODE -ne 0) { throw 'Service configuration failed.' }
Start-Service $n
(Get-Service $n).WaitForStatus('Running',[TimeSpan]::FromSeconds(15))
$a = Get-Content 'C:\NAD0520261009\heartbeat.txt' -Raw
Start-Sleep -Seconds 2
$b = Get-Content 'C:\NAD0520261009\heartbeat.txt' -Raw
if ($a -eq $b) { throw 'Heartbeat did not advance.' }
Stop-Service $n
(Get-Service $n).WaitForStatus('Stopped',[TimeSpan]::FromSeconds(15))
Get-Content 'C:\NAD0520261009\stopped.txt'Quote the executable path separately from arguments. An ordinary utility needs a supported host, scheduled task, or suitable service wrapper. Evaluate that wrapper's shutdown signals and child-process cleanup rather than assuming every wrapper can only terminate children forcibly.
4. Initialization and timeouts
On LTSC 2021, directly registering cmd.exe /c ping -n 45 127.0.0.1 as kbslow produced 1053/7009 after roughly 30 seconds. delayed-auto still failed after 30.1 seconds. Delayed automatic startup changes boot timing, not the missing service protocol.
A real ServiceBase implementation blocked inside OnStart for 45 seconds. In that run, sc start returned START_PENDING after about 0.1 seconds; it remained pending at eight seconds and was Running at 50, without 7009. That demonstrates different behavior after establishing the dispatcher connection, not a guarantee that every framework/system permits unreported 45-second initialization.
The asynchronous version logged:
18:17:51.1270045Z OnStart enter mode=async
18:17:51.1426420Z work begin
18:17:51.1426420Z OnStart return
18:18:36.1576729Z work doneIts start call returned in 0.07 seconds and Running was observed within three seconds; work completed later. Expose readiness or reject premature requests while dependencies/migrations remain incomplete. Running alone must not silently imply readiness.
For genuinely pending initialization, report bounded wait hints/progress. ServiceBase.RequestAdditionalTime is for pending operations, not a global timeout or a method valid in any state. Reflect expected work instead of mechanically decreasing every hint or renewing indefinitely around a hang. See RequestAdditionalTime.
The recorded host had no ServicesPipeTimeout override. A global override is not the default repair: fix entry/status reporting first. Legacy cases requiring it need their own baseline, impact, and reboot plan. A successful sc start or 1056 response still does not replace waiting for state and a real request.
5. Crash recovery
KBEW0720261011 ran on a Server 2022 workgroup host and logged timestamps/PID each second. Initially qfailure had no actions. After forced termination, 20 seconds of samples remained Stopped/PID=0, heartbeat stopped, and 7034 appeared.
Only recovery configuration changed; the EXE remained identical:
sc.exe failure KBEW0720261011 reset= 86400 actions= restart/1000/restart/1000/restart/1000
sc.exe qfailure KBEW0720261011
sc.exe qfailureflag KBEW0720261011The next dedicated-test crash reached START_PENDING after about 1.05 seconds and Running with a new PID after 2.11 seconds. Heartbeats resumed and 7031 named the restart action. A 1000-ms action delay is not a one-second business-recovery guarantee.
Three restart entries do not stop recovery after the third failure. Once the action list is exhausted, SCM repeats its final action. To bound a crash loop, design an explicit final no-action policy, external rate limiting, and alerting, then verify those controls; do not rely on three exhausted retries.
reset=86400 means resetting after the specified failure-free interval, not at midnight daily. Never resetting uses INFINITE semantics; reset=0 must not be described as never reset. See SERVICE_FAILURE_ACTIONS.
A normal Stop-Service remained stopped through the recorded 20-second control. Noncrash stops reporting a nonzero Win32 exit code depend on failureflag; that branch was not tested. The flag only matters with configured actions. See failure-action flag.
Do not test by killing an unconfirmed business process. Shared hosts may run other services. Use a dedicated own-process lab service, confirm path/PID/window, and record configuration, exit event, replacement PID, and output.
6. Backups and rollback
For an existing service, capture command line, identity, startup mode, and recovery settings first:
$backup = Join-Path $env:ProgramData ('ServiceBackup-' + (Get-Date -Format yyyyMMdd-HHmmss))
New-Item -ItemType Directory -Path $backup -ErrorAction Stop | Out-Null
reg.exe export "HKLM\SYSTEM\CurrentControlSet\Services\$n" "$backup\service.reg"
if ($LASTEXITCODE -ne 0) { throw 'Registry export failed.' }
Get-CimInstance Win32_Service -Filter "Name='$n'" |
Select-Object Name,PathName,StartName,StartMode | Export-Clixml "$backup\service.clixml"
sc.exe qfailure $n | Out-File "$backup\recovery.txt"
sc.exe qfailureflag $n | Out-File "$backup\recovery-flag.txt"A registry export does not contain a directly reusable plaintext service password and is not a universal recovery package. Restore appropriate configuration through SCM/service properties, managing credentials separately. Do not import an entire old service registry key over a running business service as a substitute for controlled recovery.
For cleanup, stop the dedicated service, confirm its process exited, then delete that exact service/directory. Retain historical SCM/Security events. Read back normal-stop and recovery-removal results. Windows PowerShell 5.1 has native empty-string argument limitations; clear actions through the recovery UI or a verified cmd invocation, and confirm the output rather than accepting a silent command.
Final acceptance includes state, actual identity, advancing output or real requests, stop handling, and the new error window. A repaired dispatcher does not prove valid credentials, and Running does not prove readiness. Stage-specific evidence keeps the next failure in the right part of the investigation.
