
About the author
Hi, I’m Edy Werder. I write hands-on guides about Proxmox, homelab servers, NAS, and WordPress, based on real setups I run and document.
No sponsors, no fluff—just real configs and results.
Enjoying the content?
By Edy Werder — IT Consultant & Tech Blogger
When the Exchange Server isn’t working as expected, admins need a quick and effective way to diagnose issues. This guide covers both routes I use. First, the Exchange Health Checker script, which collects everything in one pass. Then a manual PowerShell checklist to verify Exchange Server health, troubleshoot mail flow, and spot problems fast.

Quick Answer:
To check Exchange Server health, run Microsoft’s free Health Checker script from the Exchange Management Shell:
.\HealthChecker.ps1
One pass collects configuration, patch level and performance data, then writes a colour coded HTML report. For mail flow, add Test-ServiceHealth, Test-Mailflow and Get-Queue.
If you run Exchange 2016 or 2019, expect the report to flag your server as out of support and unpatched. Support ended on 14 October 2025.
Microsoft ships a free diagnostic script called Health Checker. It is part of the CSS-Exchange repository. One run collects the configuration, patch level, and performance data of a server, then flags common configuration issues against Microsoft’s best practices.
Run it before anything else. It often shows that the problem is a configuration setting rather than a fault.
Grab the latest version straight from the GitHub release:
Switch first in the PowerShell section to the directory where you want to download the script file.
PowerShell
Invoke-WebRequest -Uri "https://github.com/microsoft/CSS-Exchange/releases/latest/download/HealthChecker.ps1" -OutFile "HealthChecker.ps1"
The script has an auto-update built in. It checks for a newer release each time it starts. If you want to refresh it without running a health check, use the ScriptUpdateOnly switch:
PowerShell
.HealthChecker.ps1 -ScriptUpdateOnly
That switch does not need elevated permissions or the Exchange Management Shell. It only pulls the latest version.
On a server with no internet connectivity, the version check will fail. Skip it with -SkipVersionCheck, or download the script on a non-Exchange system and copy it over.
Before you run Health Checker, confirm your group membership. You need:
Missing one of the following groups is the usual reason the script stops early with an access error. Organization Management plus Domain Admins is not a substitute for local administrator rights, so check all three.
Health Checker supports the following Exchange Server versions:
Older builds are out of support and the script no longer targets them. Fact worth knowing: Exchange Server 2013 reached end of support in April 2023, so if you are still on it, the health check is not your biggest problem.
Building a server from scratch instead of checking an existing one? The functional level and prerequisite traps are worth reading first, and I collected mine in Install Exchange Server 2019, lessons learned.
The simplest run analyses the local server:
PowerShell
.HealthChecker.ps1
To point it at a specified server instead, pass the Server parameter:
PowerShell
PS C:> .HealthChecker.ps1 -Server EX01
The main data collection writes a detailed output to the screen, a .txt log file, and an XML file per server. The default location is the current directory. Change the output location with -OutputFilePath:
PowerShell
The Output location should already exist; the script will not run.
PS C:> .HealthChecker.ps1 -Server EX01 -OutputFilePath C:HealthCheck
To collect data from every Exchange server in the Exchange environment, pipe them in:
PowerShell
PS C:> Get-ExchangeServer | Where-Object {$_.AdminDisplayVersion -Match "^Version 15"} | .HealthChecker.ps1
Each server produces its own HealthChecker XML file in the same location. The script serverlist is built from the pipeline, so you can filter it however you like.
Once all the XML files are located in one folder, turn them into a single consolidated HTML report:
PowerShell
PS C:> .HealthChecker.ps1 -BuildHtmlServersReport -XMLDirectoryPath C:HealthCheck
The BuildHtmlServersReport switch reads the existing HealthChecker XML files and writes an HTML output file. The default name is ExchangeAllServersReport-yyyyMMddHHmmss.html. Override it with -HtmlReportFile.
The report is color coded. Red rows are errors, yellow are warnings, and those are the rows worth reading first.
The LoadBalancingReport switch shows how client connections are distributed across all the Exchange CAS endpoints:
PowerShell
PS C:> .HealthChecker.ps1 -LoadBalancingReport -SiteName SiteA
Without SiteName, it uses the current site. You can also target specific servers with -ServerList EX01,EX02. Use it to confirm a namespace is actually balanced, rather than sending every backend connection to one node.
The mailbox report returns mailbox statistics for whichever server you point it at:
PowerShell
PS C:> .HealthChecker.ps1 -Server EX01 -MailboxReport
Useful on MBX servers when you want database distribution and mailbox counts without writing your own query. For who can open what once you have that list, see Set-Mailbox permissions in Exchange Server.
The VulnerabilityReport switch helps collect known CVE exposure across the environment:
PowerShell
PS C:> .HealthChecker.ps1 -VulnerabilityReport
It exports a JSON file alongside the usual output, which makes it easy to feed into a ticket or a compliance sheet.
Switch | What it does |
|---|---|
-AnalyzeDataOnly | Reprocesses XML you already collected, without touching the servers again |
-ForceLegacy | Collects serially instead of spawning background jobs |
-SkipVersionCheck | Bypasses the script version check |
-SaveDebugLog | Retains the debug log after a clean run instead of discarding it |
-DCCoreRatio | Compares your Exchange core count against DC and GC cores |
AnalyzeDataOnly is the one worth remembering. If you already ran the collection, there is no reason to hit the servers again just to reread the results.
Use ForceLegacy on slow or heavily loaded systems, where the background jobs cause trouble. The run takes longer in exchange.
Turn on -SaveDebugLog before you raise a support case. Without it the debug log is discarded when the script finishes cleanly.
This is the part nobody warned me about, so it belongs in this guide.
I ran Health Checker on an Exchange 2019 CU15 server in September 2026 expecting the usual list of configuration warnings. What came back was a red error at the top of the report:
Your Exchange server is out of support and no longer receives SUs. It is now considered persistently vulnerable and it should be decommissioned ASAP.
Underneath it, a long list of CVEs.
The reason is simple. Exchange Server 2016 and 2019 reached end of support on 14 October 2025. If you are running either one, Health Checker will tell you the same thing.
Here is the part that surprised me most. I could not just download the latest fix. Security updates released after that date are only available through the paid Extended Security Update program, which you arrange with a Microsoft account representative. Windows Update offers you nothing. There is no warning on the server. Patching simply goes quiet, and months pass without anyone noticing.
Two things are worth knowing before you plan around this:
I have since upgraded three Exchange 2019 servers to Subscription Edition. One standalone, and two DAG members that I did one at a time. CU15 is an in-place upgrade that installs much like a normal cumulative update, so the server and hardware remain unchanged. On my own server it took about 35 minutes.
Health Checker earned its place here a second time. I ran it before starting and it flagged the prerequisites worth clearing first: page file size, outdated Visual C++ runtimes, a power plan still set to Balanced, and .NET strong crypto settings. None of them took long, and clearing them beforehand made for a clean upgrade.
Put the server into maintenance mode before you begin, exactly as you would for a cumulative update. And if setup stops partway through complaining about a certificate, that is a known trap I wrote up separately in Exchange setup fails with a certificate error.
Here is the rest of my prep before each upgrade: a fresh Veeam backup, a Proxmox snapshot of the VM, antivirus paused, and a check that the databases and queues are healthy. For the two DAG members I moved the databases to the other node first, then worked through one server at a time.
One thing worth knowing afterward: Windows Update starts working again. Once the servers were on SE I installed the current August 2026 security update the normal way, which is exactly what Exchange 2019 could no longer do.
So run Health Checker twice. Once to find out how long you have been unpatched, and again as a pre-flight check before you begin the upgrade.
Health Checker is thorough, but it does not test mail flow. These eleven checks cover the live behavior of the Exchange Server. Run them from the main PowerShell session in EMS.
Ensure all critical Exchange services are running:
PowerShell
Test-ServiceHealth
✅ Expected: All required services should be Running.
Verify if essential Exchange components are active:
PowerShell
Get-ServerComponentState -Identity $env:COMPUTERNAME
✅ Expected: Most components should be Active, except hybrid-only features like ForwardSyncDaemon.
Send a test email to check internal mail routing:
PowerShell
Test-Mailflow
✅ Expected: Success with reasonable latency.
Test-Mailflow only proves that a probe message made it through. The message tracking log shows what happened to real mail. Use it when a user reports that a message never arrived.
PowerShell
Get-MessageTrackingLog -Start (Get-Date).AddMinutes(-30) |
Where-Object { $_.EventId -in "RECEIVE", "DELIVER", "SEND" } |
Sort-Object Timestamp |
Format-Table Timestamp, EventId, Source, Sender, Recipients, MessageSubject -AutoSize
✅ Expected: every message shows a RECEIVE followed by a DELIVER or a SEND, with no gap in between.
Three things to know before you trust the output:
Ensure mailbox databases are online and healthy:
PowerShell
Get-MailboxDatabase -Status | Select Name, Mounted, OnlineMaintenanceRunning
✅ Expected: Mounted = True.
Identify stuck emails in transport queues:
PowerShell
Get-Queue
✅ Expected: Low message count, no long-standing retries.
Force a retry if needed:
PowerShell
Get-Queue | Where-Object {$_.Status -eq "Retry"} | Retry-Queue -Resubmit $true
Delete problematic messages:
PowerShell
Get-Queue | Get-Message | Remove-Message -WithNDR $false
Ensure Exchange can resolve external domains:
PowerShell
Resolve-DnsName example.com -Type MX
✅ Expected: Successful resolution with valid MX records.
Check if Exchange can send emails to external servers:
PowerShell
Test-NetConnection -ComputerName example.com -Port 25
✅ Expected: TCP connection succeeds.
Identify critical errors related to mail flow and databases:
PowerShell
Get-EventLog -LogName Application -EntryType Error -After (Get-Date).AddDays(-3) | Where-Object {$_.Source -match "MSExchange"}
✅ Expected: No recent critical errors.
Ensure web-based access is functional:
PowerShell
Test-OutlookWebServices | FL
✅ Expected: All tests return Success.
Ensure Exchange certificates are valid:
PowerShell
Get-ExchangeCertificate | Select Thumbprint, FriendlyName, Subject, NotAfter
✅ Expected: NotAfter should be in the future.
If one is close to expiring, replace it before it bites. I walk through the whole process in Renew an Exchange certificate in 7 steps.
Test-ServiceHealth is the fastest single command. It lists all required Exchange services and flags those that are not running. For a complete picture, run the Health Checker script instead, as it covers configuration, patch level, and performance issues in a single pass.
Work outward from the services. Start with Test-ServiceHealth, then Get-ServerComponentState for component status, then Get-Queue for transport. Each command targets one layer, so a failure tells you where to look next.
Run Get-MailboxDatabase -Status and confirm Mounted is True on every database. For a DAG, add Get-MailboxDatabaseCopyStatus * to check the copy queue length and replication state on each copy.
Use Get-MessageTrackingLog with -Sender or -Recipients to narrow it down, then read the EventId column in timestamp order. A message that stops after RECEIVE never left the server. One that reaches SEND left, and the answer is on the other end.
Because Exchange 2016 and 2019 reached end of support on 14 October 2025. Updates released after that date go only to servers covered by the Extended Security Update program, and they are not pushed through Windows Update. Nothing on the server tells you this has happened.
This checklist covers what to look at when an Exchange Server misbehaves. Run Health Checker for the configuration baseline, then use the PowerShell commands to confirm mail flow actually works. Between the two you can pinpoint most problems and keep email services running smoothly.
If the report indicates the server is out of support, treat that as the finding that takes precedence over everything else on the page.
I’d love to hear from you. Was this article helpful? Share your thoughts in the comments below. If you prefer, you can also reach me by email or connect with me on Reddit at Navigatetech.
Hi, I’m Edy Werder. I write hands-on guides about Proxmox, homelab servers, NAS, and WordPress, based on real setups I run and document.
No sponsors, no fluff—just real configs and results.
Enjoying the content?