85°F Knoxville
Mon–Fri 9–5 ETAnswered by a human
Automation · RESOLVED

Patch Compliance Failing: 90+ Day Uptime, Missing Updates

Audit flagged 49% of endpoints missing critical patches. Updates were installed - just waiting for reboots that never came.

Incident

Compliance audit flagged a healthcare client for "missing critical patches." 23 of 47 workstations showed patches downloaded but not installed. The audit report was damning: "49% of endpoints missing KB5034441 and KB5034763 — critical security updates." The client panicked. They were certain we'd pushed those updates months ago.

We pulled up WSUS. Every single machine showed "Installed — Pending Reboot." The patches were there. They'd been there for weeks, in some cases months. They were just waiting for a reboot that never came.

Problem identification

Fleet uptime analysis told the real story:

  • highest uptime: 97 days — front desk workstation, "we never turn it off"
  • average uptime: 34 days across 47 workstations
  • compliance status: 0% — every "installed" patch was actually pending reboot
  • root cause: Fast Startup + user behavior = months without real reboots

Windows 10's Fast Startup was the culprit. When users click "Shut Down," Windows actually hibernates the kernel to disk. The machine appears to power off, but it isn't a true reboot — the running kernel and session state are persisted and restored on the next boot. Updates sitting in "Pending Reboot" status never actually get applied, because the components that need to swap out are still loaded from the previous session.

So users would diligently shut down at 5 PM, open the laptop at 8 AM, and their "brand new" session was the same kernel they'd been running for 97 days.

Why standard solutions failed

We'd already configured Windows Update deadlines and auto-restart through policy. Here's why none of it worked:

  • Active Hours: users had set 6 AM to 11 PM (essentially "never auto-restart")
  • deadline forcing: would trigger at 2 AM, but laptops were in sleep mode (lid closed) — nothing happened
  • user prompts: "Restart now" banners dismissed for weeks — users learned to click them away without reading
  • smart scheduling: Windows picked "inactive times" when the laptop was asleep in a bag — no activity, no reboot

The fundamental problem: Windows Update can only restart a machine that's awake and online. Users who close their laptop lids at 5 PM and open them at 8 AM never hit the maintenance window. Ever.

Resolution

Needed to enforce reboots through a mechanism that actually controlled when machines restarted, regardless of Windows Update's "smart" scheduling. The RMM agent runs with SYSTEM privileges and can force a restart whether Windows Update decides it's a good time or not.

maintenance_reboot.ps1View on GitHub →
$ErrorActionPreference = 'Stop'

<#
██╗     ██╗███╗   ███╗███████╗██╗  ██╗ █████╗ ██╗    ██╗██╗  ██╗
██║     ██║████╗ ████║██╔════╝██║  ██║██╔══██╗██║    ██║██║ ██╔╝
██║     ██║██╔████╔██║█████╗  ███████║███████║██║ █╗ ██║█████╔╝
██║     ██║██║╚██╔╝██║██╔══╝  ██╔══██║██╔══██║██║███╗██║██╔═██╗
███████╗██║██║ ╚═╝ ██║███████╗██║  ██║██║  ██║╚███╔███╔╝██║  ██╗
╚══════╝╚═╝╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝╚═╝  ╚═╝ ╚══╝╚══╝ ╚═╝  ╚═╝
================================================================================
 SCRIPT   : Maintenance Reboot                                            v8.1.2
 AUTHOR   : Limehawk.io
 DATE     : January 2026
 USAGE    : .\maintenance_reboot.ps1
================================================================================
 FILE     : maintenance_reboot.ps1
 DESCRIPTION : System reboot with graceful or force mode for maintenance
--------------------------------------------------------------------------------
 README
--------------------------------------------------------------------------------
 PURPOSE

   Performs system reboot when uptime exceeds threshold or reboot flags are
   detected. Supports two modes:

   GRACEFUL MODE (default) - For servers/workstations with active users
   - Warns users before rebooting (configurable delay)
   - Skips reboot if critical apps running (QuickBooks, SQL, etc.)
   - Apps get chance to save data and close cleanly

   FORCE MODE - For after-hours maintenance windows
   - Immediate forced reboot, no warning
   - Guaranteed to reboot regardless of running apps
   - Use when machine is known to be empty

 DATA SOURCES & PRIORITY
 1) RMM literal text replacement ($maxuptimedays)
 2) Environment variable (MAXUPTIMEDAYS)
 3) Default (7 days)

 REQUIRED INPUTS
 - Graceful Reboot:
   - RMM Name: '$graceful_true_or_false' (literal text placeholder)
   - Values: 'true' or 'false'
   - Default: true (graceful mode)

 - Max Uptime Days:
   - RMM Name: '$maxuptimedays' (literal text placeholder)
   - Env Name: 'MAXUPTIMEDAYS'
   - Constraints: Integer (1 or greater)
   - Default: 7

 SETTINGS (hardcoded in script)
 - $GracefulReboot        : Use graceful mode with warning (default: true)
 - $WarningMinutes        : Minutes of warning before graceful reboot (default: 5)
 - $CheckCriticalApps     : Skip reboot if critical apps running (default: true)
 - $CriticalProcessPatterns : Process names to check (QuickBooks, SQL, etc.)
 - $DefaultMaxUptimeDays  : Default uptime threshold if RMM/env not set (7)
 - $CheckCBSRebootPending : Check Component Based Servicing flag (default: true)
 - $CheckWURebootRequired : Check Windows Update flag (default: true)
 - $CheckPendingFileRename: Check Pending File Rename Operations (default: true)

 BEHAVIOR
 - Retrieves the system's last boot time to calculate current uptime in days
 - Checks Windows registry for reboot-pending flags
 - In graceful mode: checks for critical apps, skips if running
 - In graceful mode: schedules reboot with user warning
 - In force mode: immediate forced reboot
 - If neither uptime nor flags trigger, no action taken

 PREREQUISITES
 - PowerShell 5.1+
 - Must be run with local Administrator rights

 SECURITY NOTES
 - The script does not handle secrets or API keys
 - Graceful mode allows apps to save data before closing
 - Force mode will not prompt users - use for empty machines only

 ENDPOINTS
 - Not applicable. This script performs local actions only.

 EXIT CODES
 - 0 success (reboot initiated, skipped due to critical apps, or not needed)
 - 1 failure (input validation failed or command failed)

 EXAMPLE RUN - Graceful mode with critical app detected

 [INFO] INPUT VALIDATION
 ==============================================================
 Reboot Mode              : Graceful
 Warning Minutes          : 5
 Check Critical Apps      : Yes
 Max Uptime Days          : 7

 [INFO] CRITICAL APP CHECK
 ==============================================================
 Checking for critical processes...
 Critical Apps Running    : QBW32
 SKIPPING REBOOT - Critical applications are running

 [WARN] FINAL STATUS
 ==============================================================
 REBOOT SKIPPED (Critical applications running)

 [OK] SCRIPT COMPLETED
 ==============================================================

 EXAMPLE RUN - Force mode reboot triggered

 [INFO] INPUT VALIDATION
 ==============================================================
 Reboot Mode              : Force
 Max Uptime Days          : 7

 [INFO] REBOOT FLAG CHECK
 ==============================================================
 CBS RebootPending        : Yes
 WU RebootRequired        : No
 PendingFileRename        : No
 Reboot Flags Detected    : Yes

 [INFO] UPTIME CHECK
 ==============================================================
 Last Boot Time           : 2025-12-20T03:00:00
 Current Uptime (Days)    : 8
 Threshold (Days)         : 7
 Uptime Exceeded          : Yes

 [RUN] REBOOT ACTION
 ==============================================================
 Trigger                  : Both: Reboot flags AND uptime exceeded
 Method                   : Force (immediate)
 Result                   : INITIATING REBOOT

 [OK] FINAL STATUS
 ==============================================================
 OPERATION COMPLETED SUCCESSFULLY (REBOOT INITIATED)

 [OK] SCRIPT COMPLETED
 ==============================================================
--------------------------------------------------------------------------------
 CHANGELOG
--------------------------------------------------------------------------------
 2026-01-19 v8.1.2 Updated to two-line ASCII console output style
 2026-01-16 v8.1.1 Restored runtime variables to YAML metadata
 2025-12-28 v8.1.0 Added $graceful_true_or_false RMM runtime variable for mode selection
 2025-12-28 v8.0.0 Added graceful/force mode toggle, critical app checks, warning delay
 2025-12-28 v7.2.4 Removed template placeholder cruft
 2025-12-28 v7.2.3 Fixed WMI uptime query - replaced legacy [WMI] moniker with Get-CimInstance
 2025-12-23 v7.2.2 Updated to Limehawk Script Framework
 2025-12-18 v7.2.1 Renamed from invoke_reboot_on_uptime.ps1 to maintenance_reboot.ps1
 2025-12-15 v7.2.0 Simplified configuration; Added toggle settings for each reboot flag check
 2025-12-15 v7.0.0 Added reboot flag detection
 2025-09-25 v6.0.0 Reverted conditional logic to robust IF/ELSEIF/ELSE structure
================================================================================
#>

Set-StrictMode -Version Latest

# ==== REBOOT MODE SETTINGS ====
$RMMGracefulValue     = "`$graceful_true_or_false"
$DefaultGraceful      = $true
$WarningMinutes       = 5
$CheckCriticalApps    = $true

# Critical process names - reboot skipped if any are running (graceful mode only)
# QuickBooks: QBW32, QBW64, QBDBMgrN, QBCFMonitorService
# Databases: sqlservr, mysqld, postgres, mongod
$CriticalProcessPatterns = @(
    "QBW32",
    "QBW64",
    "QBDBMgrN",
    "QBCFMonitorService",
    "sqlservr",
    "mysqld",
    "postgres",
    "mongod"
)

# ==== UPTIME AND FLAG SETTINGS ====
$DefaultMaxUptimeDays   = 7
$CheckCBSRebootPending  = $true
$CheckWURebootRequired  = $true
$CheckPendingFileRename = $true
$RMMValue               = "`$maxuptimedays"

# ==== HELPER FUNCTIONS ====
function Write-Section {
    param([string]$title, [string]$status = "INFO")
    Write-Host ""
    Write-Host ("[$status] $title")
    Write-Host ("=" * 62)
}

function PrintKV([string]$label, [string]$value) {
    $lbl = $label.PadRight(24)
    Write-Host (" {0} : {1}" -f $lbl, $value)
}

# ==== INPUT RESOLUTION (RMM -> Default) ====

# Resolve GracefulReboot (RMM -> Default)
$isGracefulRMMUsable = -not [string]::IsNullOrWhiteSpace($RMMGracefulValue) -and ($RMMGracefulValue -notmatch '^\$\w+$')
if ($isGracefulRMMUsable) {
    # RMM value provided - check if it's "true" or "false"
    $GracefulReboot = $RMMGracefulValue -match '^(true|1|yes)$'
}
else {
    $GracefulReboot = $DefaultGraceful
}

# Resolve MaxUptimeDays (RMM -> ENV -> Default)
$ResolvedInput = $null
$isRMMValueUsable = -not [string]::IsNullOrWhiteSpace($RMMValue) -and ($RMMValue -notmatch '^\$\w+$')

if ($isRMMValueUsable) {
    $ResolvedInput = $RMMValue
}
elseif (-not [string]::IsNullOrWhiteSpace($env:MAXUPTIMEDAYS)) {
    $ResolvedInput = $env:MAXUPTIMEDAYS
}
else {
    $ResolvedInput = $DefaultMaxUptimeDays
}

# ==== VALIDATE AND CAST ====
$errors = @()
if (-not ([int]::TryParse($ResolvedInput, [ref]$null) -and [int]$ResolvedInput -ge 1)) {
    $errors += "MaxUptimeDays must be an integer of 1 or greater. Value provided: '$ResolvedInput'."
}
$MaxUptimeDays = [int]$ResolvedInput

if ($errors.Count -gt 0) {
    Write-Section "ERROR OCCURRED" "ERROR"
    foreach ($e in $errors) { PrintKV "Message" $e }
    Write-Section "FINAL STATUS" "ERROR"
    Write-Host " INPUT VALIDATION FAILED"
    Write-Section "SCRIPT COMPLETED" "ERROR"
    exit 1
}

# ==== INPUT VALIDATION OUTPUT ====
Write-Section "INPUT VALIDATION"
PrintKV "Reboot Mode" $(if ($GracefulReboot) { "Graceful" } else { "Force" })
if ($GracefulReboot) {
    PrintKV "Warning Minutes" $WarningMinutes
    PrintKV "Check Critical Apps" $(if ($CheckCriticalApps) { "Yes" } else { "No" })
}
PrintKV "Max Uptime Days" $MaxUptimeDays
PrintKV "Check CBS Pending" $(if ($CheckCBSRebootPending) { "Yes" } else { "No" })
PrintKV "Check WU Required" $(if ($CheckWURebootRequired) { "Yes" } else { "No" })
PrintKV "Check File Rename" $(if ($CheckPendingFileRename) { "Yes" } else { "No" })

# ==== MAIN OPERATION ====
try {
    # --- Critical Application Check (Graceful Mode Only) ---
    if ($GracefulReboot -and $CheckCriticalApps) {
        Write-Section "CRITICAL APP CHECK" "INFO"
        Write-Host " Checking for critical processes..."

        $runningCritical = @()
        $allProcesses = Get-Process -ErrorAction SilentlyContinue

        foreach ($pattern in $CriticalProcessPatterns) {
            $found = $allProcesses | Where-Object { $_.ProcessName -like "*$pattern*" }
            if ($found) {
                foreach ($proc in $found) {
                    $runningCritical += $proc.ProcessName
                }
            }
        }

        if ($runningCritical.Count -gt 0) {
            $uniqueProcesses = $runningCritical | Select-Object -Unique
            PrintKV "Critical Apps Running" ($uniqueProcesses -join ", ")
            Write-Host ""
            Write-Host " SKIPPING REBOOT - Critical applications are running"
            Write-Host " These applications may have unsaved data or open databases."
            Write-Host " Reboot will be attempted on next scheduled run."

            Write-Section "FINAL STATUS" "WARN"
            Write-Host " REBOOT SKIPPED (Critical applications running)"

            Write-Section "SCRIPT COMPLETED" "OK"
            exit 0
        }

        Write-Host " No critical applications running"
    }

    # --- Reboot Flag Detection ---
    Write-Section "REBOOT FLAG CHECK" "INFO"

    $cbsRebootPending = $false
    if ($CheckCBSRebootPending) {
        $cbsRebootPending = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending'
    }

    $wuRebootRequired = $false
    if ($CheckWURebootRequired) {
        $wuRebootRequired = Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
    }

    $pendingFileRename = $false
    if ($CheckPendingFileRename) {
        $sessionManagerPath = 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager'
        if (Test-Path $sessionManagerPath) {
            $pendingOps = Get-ItemProperty -Path $sessionManagerPath -Name 'PendingFileRenameOperations' -ErrorAction SilentlyContinue
            if ($pendingOps -and $pendingOps.PendingFileRenameOperations) {
                $pendingFileRename = $true
            }
        }
    }

    $rebootFlagsDetected = $cbsRebootPending -or $wuRebootRequired -or $pendingFileRename

    PrintKV "CBS RebootPending" $(if (-not $CheckCBSRebootPending) { "Skipped" } elseif ($cbsRebootPending) { "Yes" } else { "No" })
    PrintKV "WU RebootRequired" $(if (-not $CheckWURebootRequired) { "Skipped" } elseif ($wuRebootRequired) { "Yes" } else { "No" })
    PrintKV "PendingFileRename" $(if (-not $CheckPendingFileRename) { "Skipped" } elseif ($pendingFileRename) { "Yes" } else { "No" })
    PrintKV "Reboot Flags Detected" $(if ($rebootFlagsDetected) { "Yes" } else { "No" })

    # --- Uptime Check ---
    Write-Section "UPTIME CHECK" "INFO"

    $os = Get-CimInstance Win32_OperatingSystem
    $bootTime = $os.LastBootUpTime
    $uptimeDays = ((Get-Date) - $bootTime).Days
    $bootTimeString = $bootTime.ToString('yyyy-MM-ddTHH:mm:ss')

    PrintKV "Last Boot Time" $bootTimeString
    PrintKV "Current Uptime (Days)" $uptimeDays
    PrintKV "Threshold (Days)" $MaxUptimeDays

    $uptimeExceeded = $uptimeDays -gt $MaxUptimeDays
    PrintKV "Uptime Exceeded" $(if ($uptimeExceeded) { "Yes" } else { "No" })

    # --- Reboot Decision ---
    if ($rebootFlagsDetected -or $uptimeExceeded) {
        Write-Section "REBOOT ACTION" "RUN"

        # Determine trigger reason
        if ($rebootFlagsDetected -and $uptimeExceeded) {
            PrintKV "Trigger" "Both: Reboot flags AND uptime exceeded"
        }
        elseif ($rebootFlagsDetected) {
            PrintKV "Trigger" "Reboot flags detected"
        }
        else {
            PrintKV "Trigger" "Uptime exceeds threshold"
        }

        if ($GracefulReboot) {
            # Graceful: schedule shutdown with warning
            $warningSeconds = $WarningMinutes * 60
            $shutdownMessage = "System will restart for maintenance in $WarningMinutes minutes. Please save your work."

            PrintKV "Method" "Graceful ($WarningMinutes min warning)"
            PrintKV "Result" "SCHEDULING REBOOT"

            $shutdownArgs = "/r /t $warningSeconds /c `"$shutdownMessage`" /d p:0:0"
            Start-Process -FilePath "shutdown.exe" -ArgumentList $shutdownArgs -NoNewWindow -Wait

            Write-Section "FINAL STATUS" "OK"
            Write-Host " OPERATION COMPLETED SUCCESSFULLY (REBOOT SCHEDULED IN $WarningMinutes MINUTES)"
        }
        else {
            # Force: immediate reboot
            PrintKV "Method" "Force (immediate)"
            PrintKV "Result" "INITIATING REBOOT"

            Restart-Computer -Force -ErrorAction Stop

            Write-Section "FINAL STATUS" "OK"
            Write-Host " OPERATION COMPLETED SUCCESSFULLY (REBOOT INITIATED)"
        }
    }
    else {
        Write-Section "FINAL STATUS" "OK"
        Write-Host " NO REBOOT REQUIRED. Uptime within threshold and no pending flags."
    }

    Write-Section "SCRIPT COMPLETED" "OK"
    exit 0
}
catch {
    Write-Section "ERROR OCCURRED" "ERROR"
    PrintKV "Step" "Main Operation"
    PrintKV "Error Type" $_.Exception.GetType().Name
    PrintKV "Error Message" $_.Exception.Message

    Write-Section "FINAL STATUS" "ERROR"
    Write-Host " OPERATION FAILED"

    Write-Section "SCRIPT COMPLETED" "ERROR"
    exit 1
}

Why this works

The script uses WMI to query the actual kernel boot time — not the last "shutdown" time that Fast Startup fakes. It checks whether the machine has exceeded an uptime threshold, and if so, forces an immediate reboot after a configurable warning period.

Technical details:

  1. Win32_OperatingSystem: returns the real LastBootUpTime — immune to Fast Startup hibernation
  2. user session detection: uses quser to enumerate logged-in users, logs who was on the machine before the reboot
  3. active user skip: optional $SkipIfUserActive flag to defer reboot if someone is actively working
  4. warning period: configurable 5-minute warning gives users time to save work before reboot

Key insight: the RMM agent can wake a machine from sleep to run scheduled tasks. Combined with this script, machines that "never reboot" now reboot weekly — automatically, during off-hours, with no user action required. The lid-closed-in-a-bag problem goes away because the agent wakes the machine, runs the check, and puts it back to sleep when done.

Outcome

Deployed the script across the fleet with a 7-day uptime threshold. Within one week, every machine had rebooted at least once. Average uptime dropped from 34 days to 4 days. The patches that had been "pending reboot" for months finally installed on the first real reboot cycle. Re-ran the compliance scan: 100% compliant. Audit passed.

  • average uptime before: 34 days (unpatched endpoints)
  • average uptime after: 4 days (within policy threshold)
  • compliance rate: 0% → 100%

Key takeaways