SCRIPT LIBRARY · POWERSHELL
A Polite Restart Reminder for PCs That Never Reboot
A small Windows Forms pop-up that appears at logon when a PC has been up for more than a week, with a Later button and a Restart now button that asks before it does anything.
- What it does
- Checks how long Windows has been running and, past a threshold, shows a small always-on-top window with the uptime and two buttons. Later dismisses it (or snoozes it with -RemindAgainMinutes). Restart now asks for confirmation, then restarts with a cancellable countdown.
- Requires
- Windows PowerShell 5.1, or PowerShell 7+ on Windows (Windows Forms needs STA, the default for both)
- No modules
- Permissions
- None. It runs as the signed-in user, and standard users can restart their own workstation.
- Runs on
- Windows 10/11
- Tested
- Parse-checked in PowerShell 7.4; the uptime check was run with a mocked boot time. The Windows Forms window and the restart are Windows-only and weren't run here
Part 8 of the thread Spring cleaning for Windows PCs
Some machines just don't get restarted. Sleep, wake, sleep, wake, for six weeks, while memory slowly leaks, a pending update waits for a reboot that never comes, and the Start menu gets a little weirder every day. Then it's slow, and the first question is always "when did you last restart it?"
My first go at this was a pop-up with no close button that nobody could get rid of, which is a great way to make people hate a pop-up. This one is friendlier. It shows up only when the machine has been running longer than -Days, says how long, and offers two buttons. Later goes away. Restart now asks "are you sure?" and then gives you a 60-second countdown that shutdown /a can still cancel.
It's made to run as a scheduled task at logon, so the reminder turns up at the moment someone sits down, not in the middle of a call.
<#
.SYNOPSIS
Pops up a friendly "time to restart" reminder when the PC has been up longer than -Days.
.DESCRIPTION
Works out how long Windows has been running from the last boot time. If that's under -Days it
exits quietly. Otherwise it shows a small window with the uptime and two buttons: Later, which
dismisses it, and Restart now, which asks for confirmation and then restarts with a short
countdown (shutdown /a cancels it). With -RemindAgainMinutes, choosing Later brings it back after
that many minutes instead of ending.
Made to run as a scheduled task at logon, in the signed-in user's session. -WhatIf shows the
reminder but only reports the restart instead of doing it. Returns an object describing what happened.
.PARAMETER Days
Show the reminder when uptime is at least this many days. Default: 7.
.PARAMETER RemindAgainMinutes
After Later, wait this long and ask again. 0 (the default) means Later ends it until next time.
.PARAMETER RestartDelaySeconds
Countdown between confirming and the restart, so open apps get a moment. Default: 60.
.PARAMETER Title
Window title. Default: Time for a restart.
.PARAMETER AlwaysShow
Show the reminder whatever the uptime. Handy for testing how it looks.
.EXAMPLE
.\Show-UptimeReminder.ps1 -Days 5
.EXAMPLE
.\Show-UptimeReminder.ps1 -AlwaysShow -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidateRange(1, 365)]
[int]$Days = 7,
[ValidateRange(0, 1440)]
[int]$RemindAgainMinutes = 0,
[ValidateRange(0, 3600)]
[int]$RestartDelaySeconds = 60,
[ValidateNotNullOrEmpty()]
[string]$Title = 'Time for a restart',
[switch]$AlwaysShow
)
$lastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem -Property LastBootUpTime).LastBootUpTime
$uptime = (Get-Date) - $lastBoot
$result = [pscustomobject]@{
ComputerName = $env:COMPUTERNAME
LastBoot = $lastBoot
UptimeDays = [math]::Round($uptime.TotalDays, 1)
ThresholdDays = $Days
Response = 'NotNeeded'
}
if ($uptime.TotalDays -lt $Days -and -not $AlwaysShow) { return $result }
if ([Threading.Thread]::CurrentThread.GetApartmentState() -ne 'STA') {
Write-Warning 'Windows Forms needs a single-threaded apartment. Run this with powershell.exe, or pwsh -STA.'
}
Add-Type -AssemblyName System.Windows.Forms, System.Drawing -ErrorAction Stop
[System.Windows.Forms.Application]::EnableVisualStyles()
function Show-Reminder {
$upText = if ($uptime.TotalDays -ge 2) { '{0} days' -f [math]::Floor($uptime.TotalDays) } else { '{0} hours' -f [math]::Floor($uptime.TotalHours) }
$font = [System.Drawing.Font]::new('Segoe UI', 10)
$form = [System.Windows.Forms.Form]@{
Text = $Title
ClientSize = [System.Drawing.Size]::new(440, 170)
StartPosition = 'CenterScreen'
FormBorderStyle = 'FixedDialog'
MaximizeBox = $false
MinimizeBox = $false
TopMost = $true
ShowInTaskbar = $true
Font = $font
}
$icon = [System.Windows.Forms.PictureBox]@{
Image = [System.Drawing.SystemIcons]::Information.ToBitmap()
SizeMode = 'AutoSize'
Location = [System.Drawing.Point]::new(20, 22)
}
$heading = [System.Windows.Forms.Label]@{
Text = "This PC has been running for $upText."
Font = [System.Drawing.Font]::new('Segoe UI', 11, [System.Drawing.FontStyle]::Bold)
Location = [System.Drawing.Point]::new(64, 20)
AutoSize = $true
}
$body = [System.Windows.Forms.Label]@{
Text = "A restart clears out memory leaks and finishes installing updates. Save your work and restart when it suits you. (Last started $($lastBoot.ToString('g')).)"
Location = [System.Drawing.Point]::new(64, 50)
Size = [System.Drawing.Size]::new(356, 60)
}
$later = [System.Windows.Forms.Button]@{
Text = 'Later'
Size = [System.Drawing.Size]::new(110, 32)
Location = [System.Drawing.Point]::new(190, 122)
DialogResult = 'Cancel'
}
$restart = [System.Windows.Forms.Button]@{
Text = 'Restart now'
Size = [System.Drawing.Size]::new(110, 32)
Location = [System.Drawing.Point]::new(310, 122)
DialogResult = 'OK'
}
$form.Controls.AddRange(@($icon, $heading, $body, $later, $restart))
$form.AcceptButton = $later # Enter shouldn't restart anyone by accident
$form.CancelButton = $later # Esc and the X both mean Later
try { $form.ShowDialog() } finally { $form.Dispose(); $font.Dispose() }
}
while ($true) {
$choice = Show-Reminder
if ($choice -eq [System.Windows.Forms.DialogResult]::OK) {
$confirm = [System.Windows.Forms.MessageBox]::Show(
"Restart in $RestartDelaySeconds seconds? Anything unsaved will be lost.",
$Title, 'YesNo', 'Warning', 'Button2')
if ($confirm -eq [System.Windows.Forms.DialogResult]::Yes) {
if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Restart in $RestartDelaySeconds seconds")) {
try {
& "$env:SystemRoot\System32\shutdown.exe" /r /t $RestartDelaySeconds /c "Restarting in $RestartDelaySeconds seconds. Run 'shutdown /a' to cancel." /d p:0:0
if ($LASTEXITCODE -ne 0) { throw "shutdown.exe exited with code $LASTEXITCODE" }
$result.Response = 'Restart'
}
catch {
$result.Response = "RestartFailed: $($_.Exception.Message)"
Write-Warning $result.Response
}
}
else { $result.Response = 'Restart (WhatIf)' }
break
}
continue # They said no to the confirmation; show the reminder again.
}
$result.Response = 'Later'
if ($RemindAgainMinutes -le 0) { break }
Start-Sleep -Seconds ($RemindAgainMinutes * 60)
$uptime = (Get-Date) - $lastBoot
}
$result
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Days | int | 7 | Show the reminder when the PC has been up at least this many days. |
-RemindAgainMinutes | int | 0 | After Later, wait this long and ask again. 0 means Later ends it until the next logon. |
-RestartDelaySeconds | int | 60 | The countdown between confirming and the restart, so open apps get a moment. |
-Title | string | Time for a restart | The window title and message box caption. |
-AlwaysShow | switch | — | Show it regardless of uptime. For seeing what it looks like. |
Run it
Try it out without restarting anything.
.\Show-UptimeReminder.ps1 -AlwaysShow -WhatIfNag after five days, and again every four hours until they give in.
.\Show-UptimeReminder.ps1 -Days 5 -RemindAgainMinutes 240Run it for everyone at logon with a scheduled task (from an elevated prompt).
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument '-NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "C:\Scripts\Show-UptimeReminder.ps1" -Days 7'; Register-ScheduledTask -TaskName 'Uptime reminder' -Action $action -Trigger (New-ScheduledTaskTrigger -AtLogOn) -Principal (New-ScheduledTaskPrincipal -GroupId 'BUILTIN\Users' -RunLevel Limited)Just check the uptime, no window.
.\Show-UptimeReminder.ps1 -Days 365What you'll see
ComputerName : PC-0142
LastBoot : 9/2/2026 7:48:13 AM
UptimeDays : 27.3
ThresholdDays : 7
Response : Later
How it works
- Measure uptime. It reads
LastBootUpTimefromWin32_OperatingSystemand subtracts it from now. Under-Days, it returns a result withResponseset to NotNeeded and exits without loading any UI. - Build a small form. Windows Forms, an information icon, one bold line with the uptime ("27 days"), one line of explanation with the last boot time, and two buttons. Later is the accept and cancel button, so Enter, Esc and the X all mean Later.
- Confirm before restarting. Restart now opens a Yes/No box with No as the default. Yes goes through
ShouldProcess, so-WhatIfjust reports it, and then runsshutdown /r /t 60with a comment telling people how to cancel. - Snooze if asked. With
-RemindAgainMinutes, Later sleeps that long and shows the window again with the uptime updated. Without it, Later ends the script until the next logon. - Report. It returns the computer name, last boot, uptime and what the user chose, which is handy if you run it from a deployment tool and want to log responses.
If a machine does restart and you want to know whether it was the user, an update or a crash, the reboot history script reads it straight out of the event log.
Take it further
- Only nag when it matters. Check the
RebootRequiredkey underHKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Updatetoo, and show a stronger message when an update is actually waiting. - Escalate gently. Pass a lower
-RemindAgainMinutesas uptime climbs, so day eight is a nudge and day twenty is a firm tap on the shoulder. - Collect the answers. Append the returned object to a CSV on a share to see which machines keep choosing Later.
Things that'll trip you up
- Fast Startup doesn't reset the clock. On Windows 10 and 11 with Fast Startup on, Shut down is really a hibernate, so uptime keeps counting through a "shutdown". Only Restart starts it over. That's why someone who swears they shut down every night can show 30 days of uptime.
- It has to run as the user, in their session. A task running as SYSTEM, or set to "run whether the user is logged on or not", shows the window in an invisible session. Use a logon trigger with the BUILTIN\Users group, like the example, so it runs as whoever signed in.
- The console window may flash. powershell.exe -WindowStyle Hidden still shows a console for a split second as it starts. It's cosmetic. If it bothers you, have the task run conhost.exe --headless powershell.exe with the same arguments.
- Enter means Later, on purpose. Both the default button and Esc are Later, so someone typing into another window when it pops up can't restart the machine by accident. Restart always needs a click and a Yes.