Task Manager on a busy Exchange server can look alarming: store processes holding tens of gigabytes, a row of w3wp.exe instances and noderunner.exe near the top of the CPU column. Some of that is by design and some is a symptom. In this post I'll separate the two on Exchange Server SE and Exchange 2019, then show a repeatable way to find what's really loading the server and prove the fix.
Errors this guide fixes
Windows successfully diagnosed a low virtual memory condition. The following programs consumed the most virtual memory:System log (Event ID 2004)452 4.3.1 Insufficient system resourcesNDR / queue LastError+3 more guidesEvent ID 2004 (Resource-Exhaustion-Detector)System log
Symptoms#
- Memory use sits close to the installed total, with
Microsoft.Exchange.Store.Worker.exeprocesses at the top. - CPU stays high for long periods, led by
noderunner.exe,w3wp.exe,EdgeTransport.exeorMSExchangeHMWorker.exe. - Outlook and Outlook on the web feel slow, queues build up, and sending servers receive
452 4.3.1 Insufficient system resourcesonce transport back pressure starts. - The System log shows Event ID 2004 (Resource-Exhaustion-Detector):
Windows successfully diagnosed a low virtual memory condition. The following programs consumed the most virtual memory: ...Why it happens: normal versus not#
| Process | What it is | Normal | Investigate when |
|---|---|---|---|
Microsoft.Exchange.Store.Worker.exe | One per database copy; holds the database cache | Large, steady memory use; Exchange 2019 and SE shift cache towards active copies (Dynamic Database Cache) | CPU stays high together with slow clients |
noderunner.exe | Exchange Search (Search Host Controller service) | Busy after mailbox moves, new databases, reseeds or updates | Busy for days, or the content index isn't healthy |
w3wp.exe | One per IIS app pool (OWA, EWS, ActiveSync, MAPI and more) | Moderate, with peaks at logon time | One pool pinned for hours |
EdgeTransport.exe | Transport worker process | Grows with queue size | Queues growing, back pressure |
MSExchangeHMWorker.exe | Managed Availability worker | Short bursts | Sustained load or repeated recovery actions |
High memory alone isn't a fault: Exchange sizes its caches to use the RAM it's given and keeps it. Real problems usually come from one of these:
- Undersized or misconfigured hardware. For Exchange 2019 and SE, Microsoft recommends at least 128 GB of RAM for the Mailbox role (Exchange supports up to 256 GB) and up to two processor sockets on physical servers; add the High performance power plan and a fixed-size page file. Dynamic memory and memory overcommit aren't supported for Exchange virtual machines.
- One client or application hammering a protocol, such as an EWS integration polling every mailbox or a misbehaving mobile device.
- Search catching up after a migration or an index rebuild.
- File-level antivirus scanning Exchange databases, logs and processes without the exclusions Microsoft documents.
- Bugs fixed in updates. For example, lock contention in
MSExchangeMapiMailboxAppPoolcould hold CPU at 100% for over an hour; the fix shipped in Exchange 2019 CU7 and Exchange 2016 CU18.
How to fix it#
1. Rule out configuration first#
Run the Health Checker and fix what it flags (power plan, memory, page file, processors, build) before chasing processes. For the page file, Exchange 2019 and SE want the minimum and maximum both set to 25% of installed RAM; Exchange 2016 wants RAM plus 10 MB, capped at 32,778 MB. Configure only one page file.
2. Identify the process and what it serves#
(Get-Counter '\Process(*)\% Processor Time').CounterSamples |
Where-Object InstanceName -notin '_total','idle' | Sort-Object CookedValue -Descending |
Select-Object -First 10 InstanceName, @{n='CPU %';e={[math]::Round($_.CookedValue / $env:NUMBER_OF_PROCESSORS, 1)}}
& "$env:windir\system32\inetsrv\appcmd.exe" list wpPer-process counters add up across all logical processors, so the script divides by their number. appcmd list wp maps each w3wp.exe process ID to its app pool, such as MSExchangeServicesAppPool (EWS) or MSExchangeSyncAppPool (ActiveSync). Task Manager's Command line column shows the same.
3. Act on the cause#
- An IIS app pool: find out who is calling it. Exchange's IIS logs are in
C:\inetpub\logs\LogFiles\W3SVC1(Default Web Site) andW3SVC2(Exchange Back End) by default. The script below lists the top EWS callers in the newest front-end log. Fix or throttle the app or device it reveals instead of just recycling the pool. - noderunner.exe: run
Get-MailboxDatabaseCopyStatus -Server EX01 | Format-Table Name, Status, ContentIndexState. If indexing is catching up after a migration, let it finish; if an index is Failed, repair it rather than stopping the search services. - EdgeTransport.exe: check
Get-Queue | Sort-Object MessageCount -Descendingand the back pressure state withGet-ExchangeDiagnosticInfo -Server EX01 -Process EdgeTransport -Component ResourceThrottling. - MSExchangeHMWorker.exe: check what Managed Availability keeps doing, as shown in my Managed Availability deep dive.
- Everything slow at once: confirm the antivirus exclusions and the build, then install the latest cumulative and security updates.
$log = Get-ChildItem 'C:\inetpub\logs\LogFiles\W3SVC1' -Filter *.log | Sort-Object LastWriteTime | Select-Object -Last 1
$fields = ((Get-Content $log.FullName -TotalCount 5) -match '^#Fields:') -replace '^#Fields: ' -split ' '
Get-Content $log.FullName | Where-Object { $_ -notmatch '^#' } | ConvertFrom-Csv -Delimiter ' ' -Header $fields |
Where-Object { $_.'cs-uri-stem' -like '/EWS/*' } | Group-Object 'cs-username','cs(User-Agent)' |
Sort-Object Count -Descending | Select-Object -First 10 Count, Name4. Capture evidence for intermittent spikes#
Exchange already writes performance logs to %ExchangeInstallPath%Logging\Diagnostics\DailyPerformanceLogs; open them in Performance Monitor to look back at a spike. For a targeted capture, use ExPerfWiz from Microsoft's CSS-Exchange scripts (its page lists Exchange 2013, 2016 and 2019):
.\ExPerfWiz.ps1 # run elevated; loads the ExPerfWiz functions
New-ExPerfWiz -FolderPath D:\ExPerfWiz -StartOnCreate
Stop-ExPerfWiz # stop it this way, not from the Performance Monitor GUIBy default it samples every 5 seconds and stops after 8 hours. Note the times users report slowness so you can line them up with the counters.
Verify the fix#
Get-Counter '\Processor(_Total)\% Processor Time' -SampleInterval 5 -MaxSamples 12stays well clear of saturation during business hours.\Memory\Available MBytesis stable and no new event 2004 appears in the System log.- Queues drain in
Get-Queueand transport reports no back pressure. Get-HealthReport -Identity EX01 | Where-Object AlertValue -ne 'Healthy'returns nothing, and the Health Checker is clean.
Prevent it next time#
- Size servers with Microsoft's Exchange sizing calculator and give virtual machines static, reserved memory.
- Give each integration its own account so IIS logs show who is calling.
- Re-check antivirus exclusions after every security agent update.
- Stay current on updates. Exchange 2016 and 2019 have been out of support since 14 October 2025, so plan your move if you're still on them.

