A CPU pegged at 100% because real customers are hammering the service is a success story. A CPU pegged at 100% because a loop is spinning on nothing is a bug wearing the same costume.
Lesson 316's leak had a quiet, patient signature — memory climbing over hours or days while everything else looked normal. High CPU is the opposite kind of alarm: loud, immediate, and visible on any dashboard within seconds. A container's CPU graph pins itself to the ceiling, requests start queuing behind it, and the natural first reaction is to assume something is broken.
Except sometimes nothing is broken at all — sometimes the CPU is at 100% because the application is doing exactly what it's supposed to be doing, just under more load than usual. This lesson teaches you to tell the difference between legitimate high CPU and pathological high CPU, the specific bug patterns that cause the pathological kind, and how to use dotnet-trace — the applied, production version of the sampling profiler concept lesson 233 introduced — to find exactly which method is actually burning the cycles.
High CPU, as a production symptom, just means one or more processor cores are spending most of their time actively executing your application's code rather than sitting idle. That fact alone tells you nothing about whether it's a problem — the real question is always why the CPU is busy: doing useful work that genuinely needs to happen, or doing wasted work that shouldn't be happening at all.
Legitimate high CPU is processor time spent on genuine, necessary computation — serializing real payloads, running real business logic, handling a real spike in traffic. Pathological high CPU is processor time spent on work that accomplishes nothing useful, or accomplishes it in a needlessly expensive way: a loop that spins instead of waiting, a retry storm without backoff, a regular expression whose matching engine has gone exponential on a particular input. Both look identical on a bare CPU-percentage graph. Distinguishing them requires looking inside the process, not just at the metric.
Just as lesson 316 named a short list of leak patterns worth memorizing, pathological high CPU tends to come from a similarly short, recurring list:
What ties these together is that none of them are "the CPU is broken" — every one is application code doing more work than it needs to, often invisibly, because the code that triggers it looks completely ordinary at a glance. This is exactly the same shape of trap lesson 218 warned about with sync-over-async: the dangerous code doesn't look dangerous.
dotnet-trace collect -p <pid> --duration 00:00:30 — captures a real, sampled CPU profile from the live process over a fixed window, using the same low-overhead sampling approach lesson 233 explained, safe to run against production.This is one of the most notorious pathological-CPU patterns in any language with a backtracking regex engine, .NET's included — and it's genuinely invisible by reading the pattern casually:
// Looks like an ordinary "one or more groups of letters" validator.
// On most inputs, it's instant. On a carefully (or accidentally) crafted
// input like "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaX",
// the nested quantifiers force the engine to try an exponential number
// of ways to backtrack across overlapping matches before giving up.
var isValid = Regex.IsMatch(input, @"^(a+)+$");
// A rewritten, non-catastrophic pattern that expresses the same intent
// without nested, ambiguous repetition — or, better, avoid a hand-rolled
// backtracking pattern for this kind of check entirely.
var isValidFixed = Regex.IsMatch(input, @"^a+$");
// .NET also lets you cap how long any single match attempt is allowed
// to run, converting a runaway match into a controlled, catchable failure
// instead of a CPU-pinned thread:
var regex = new Regex(@"^(a+)+$", RegexOptions.None, TimeSpan.FromMilliseconds(500));Meaning: The buggy pattern and the fixed pattern accept exactly the same set of "all letter a" strings on ordinary input — the difference only shows up on adversarial or unusually long input, which is exactly why this class of bug so often reaches production undetected: it passed every normal test case fine.
An ASP.NET Core API's CPU climbs to 100% across every instance shortly after a deploy that added a new email-format validation rule to a minimal API endpoint (lesson 256). Traffic hasn't changed — the deploy-correlation question from lesson 315's triage framework flags this immediately. A dotnet-trace capture, opened as a flame graph, shows over 90% of samples sitting inside a single call to Regex.IsMatch deep inside the new validator — and one particular customer, sending a slightly malformed email address in a batch import, happens to trigger exactly the catastrophic-backtracking shape from the Simple Example above. Every request through that validator with that kind of input pins a thread pool worker at 100% CPU for seconds at a time, and under enough concurrent requests, the whole service's CPU saturates. Notice what didn't help here: more replicas, more CPU cores, a bigger instance size — none of it fixes a bug that gets exponentially worse with input length. Only fixing the pattern does.
Picture a car's tachometer pinned near the redline. That alone tells you nothing about whether the car is doing anything useful — it could be climbing a steep hill at full power, genuinely working hard and making real progress (legitimate high CPU), or it could be stuck in a ditch with the wheels spinning uselessly in mud, engine screaming, going absolutely nowhere (pathological high CPU). From the dashboard alone, both look identical: RPM maxed. The only way to tell them apart is to look past the tachometer at whether the car is actually moving — the equivalent of checking whether throughput is climbing along with CPU, and then popping the hood (a flame graph) to see exactly which part of the engine is working, and whether that work is going anywhere.
Recall from lesson 233 how a sampling profiler works: it interrupts the running process hundreds of times per second and records exactly what's on the call stack at that instant, building a statistical picture from many samples rather than instrumenting every call. dotnet-trace is the CLI front end for capturing exactly this kind of sampled data from a live process via EventPipe, without the heavier overhead an instrumentation profiler would add. A flame graph is simply that raw sample data rendered visually: each frame's width is proportional to how many of the total samples included that method somewhere on the stack, and stacking frames vertically shows the actual call chain — so the widest bar at the very top of a tall stack is, statistically, exactly where the process's cycles are actually going, distinguishing "self time" (this method's own code) from "total time" (this method plus everything it called).
Scaling out or up genuinely helps with legitimate high CPU under real load — that's exactly what it's for. It does nothing at all for pathological high CPU, and can actively hide the bug: more replicas just mean more instances each independently burning cycles on the same wasted work, at proportionally higher cost, while the root cause ships untouched.
Worth flagging early, ahead of lessons 318 and 319: a genuine deadlock (218, 318) shows threads permanently blocked, doing nothing at all — CPU usage for those specific threads sits near zero, not high. High CPU and a hang from blocked/waiting threads are near-opposite symptoms, even though both can make an application feel "stuck" to a user waiting on a response.
Wrapping a downstream call in a retry loop that fires again immediately on failure, with no delay — under a real outage, this turns one failing dependency into a CPU (and network) storm across every instance retrying it simultaneously.
Use exponential backoff between retry attempts (lesson 291), and pair it with a circuit breaker (lesson 292) so a persistently failing dependency stops being hammered at all after enough consecutive failures.
Polling a flag or condition in a tight while loop with no delay or yield, burning an entire core just to notice a change the instant it happens.
Use a real waiting primitive — an await on a task, a SemaphoreSlim, a Channel (lesson 214) — that actually releases the thread while nothing has changed, instead of spinning.
Writing a validation pattern that works fine on every normal test case, without considering what happens on a pathological or maliciously crafted input.
Avoid nested repetition where possible, set a matching timeout on any regex processing untrusted input, and specifically test validators against long, adversarial strings — not just the happy path.
dotnet-trace to find out whether the work being done is real or wasted before assuming either.dotnet-trace captures a real, sampled CPU profile from a live process — the same low-overhead sampling concept from lesson 233, applied here to a live production investigation — and a flame graph makes the dominant, self-time-heavy method visible at a glance.You've seen how to tell legitimate CPU use apart from a pathological bug, and how to find the culprit with dotnet-trace. Let's confirm it landed.
1. Two services both show CPU pinned at 100%. Service A's throughput graph shows a matching spike in real traffic. Service B's throughput graph is flat, with no traffic increase at all. What does this difference suggest?
Correct: B
Why B is correct: This is exactly the correlation check the lesson's diagnosis flow opens with — CPU that rises alongside real traffic looks legitimate; CPU pinned high with flat throughput is the strongest early signal that the work being done isn't producing proportional useful output, pointing toward a pathological cause.
Why A is incorrect: Throughput correlation is precisely the first, cheapest signal the lesson recommends checking — it's far from irrelevant.
Why C is incorrect: A deadlock's signature is near-zero CPU with blocked threads, not high CPU — high CPU with flat throughput points toward pathological CPU use (this lesson), not a deadlock (218/318).
Why D is incorrect: Both being at 100% CPU is exactly why further investigation matters — the graph alone can't tell you whether either one is a real problem without correlating against throughput and, ideally, a flame graph.
Reinforcement: Always correlate CPU against throughput first — it's the fastest way to separate "probably fine" from "probably a bug."
2. A flame graph captured via dotnet-trace shows over 85% of samples concentrated in a single, narrow method deep in the call stack, unrelated to any change in traffic volume. What does this most likely indicate?
Correct: B
Why B is correct: This is exactly the flame-graph reading pattern the lesson describes — a legitimate workload's time is usually spread across genuine business logic, while a single narrow method dominating the graph (especially disconnected from a traffic increase) is the classic fingerprint of pathological, wasted CPU work.
Why A is incorrect: A single dominant method with no traffic correlation is specifically flagged in the lesson as suspicious, not as normal healthy behavior.
Why C is incorrect: A flame graph legitimately dominated by one method is a valid, meaningful, and common result — it's exactly what points an investigator toward the actual bug, not a sign of tool misconfiguration.
Why D is incorrect: A CPU flame graph says nothing about heap growth or reachability — that's lesson 316's concern, diagnosed with dotnet-gcdump, an entirely different tool and symptom.
Reinforcement: A narrow, dominant hot method in a flame graph — especially with no matching traffic increase — is the fingerprint of pathological CPU use.
3. A team's response to sustained high CPU is to add more server replicas. If the underlying cause is a busy-wait loop bug, what will happen?
Correct: B
Why B is correct: This is exactly the point the lesson makes about pathological CPU — scaling out helps legitimate load, but for a genuine bug like a busy-wait loop, every new replica just independently reproduces the same wasted work, raising cost without touching the actual cause.
Why A is incorrect: More capacity only helps when the CPU usage is legitimate and proportional to real work — a bug that wastes cycles wastes them on every instance regardless of how many you add.
Why C is incorrect: Scaling out and a memory leak are unrelated concerns — adding replicas has no direct mechanism for causing a leak (lesson 316's topic) on any of them.
Why D is incorrect: Scaling infrastructure has no ability to inspect or patch application code — the fix for a busy-wait loop requires an actual code change, not more instances.
Reinforcement: Scaling is the right lever for legitimate load, never a substitute for fixing a genuine CPU-wasting bug.
4. A retry mechanism calls a failing downstream dependency again immediately on every failure, with no delay between attempts. During an outage of that dependency, what's the likely CPU/network consequence, and what's the fix?
Correct: B
Why B is correct: This matches the lesson's explicit Mistake 1 — an immediate, backoff-free retry loop wastes CPU and network capacity hammering a dependency that's already failing; exponential backoff spaces out attempts, and a circuit breaker stops the hammering altogether once failure becomes persistent.
Why A is incorrect: This directly contradicts the lesson's point — a tight retry loop with no delay is specifically named as a pathological-CPU cause, not a safe default.
Why C is incorrect: Increasing retry count without adding backoff makes the problem worse, not better — more immediate retries mean more wasted CPU and network calls against an already-struggling dependency.
Why D is incorrect: This scenario is explicitly a CPU/network concern in the lesson (retries burning cycles and bandwidth) — it has no particular connection to heap reachability or memory leaks.
Reinforcement: Backoff and circuit breakers aren't just resilience patterns — they directly prevent a specific class of pathological CPU spike.
5. A support engineer reports "the app is completely stuck — requests never come back, and CPU usage across all threads is near zero." Based on this lesson, is this consistent with the high-CPU failure mode covered here?
Correct: B
Why B is correct: The lesson explicitly flags this distinction — near-zero CPU with hanging requests is the opposite symptom profile from high CPU, and points toward threads that are blocked or idle rather than busy, which is exactly what the next two lessons (deadlocks and thread pool starvation) diagnose.
Why A is incorrect: This lesson is specifically about CPU that's busy, whether legitimately or pathologically — a hang with idle CPU is a fundamentally different symptom, covered by different lessons.
Why C is incorrect: A busy-wait loop is defined by actively spinning the CPU, checking a condition repeatedly — that produces high CPU, not near-zero CPU, which is the exact opposite of what's described here.
Why D is incorrect: dotnet-gcdump diagnoses heap growth (lesson 316's tool), which has no direct bearing on diagnosing a hang with idle CPU — that scenario calls for the thread/lock analysis covered in lessons 318 and 319.
Reinforcement: High CPU and a near-zero-CPU hang are opposite signatures — recognizing which one you're looking at is what routes you to the right lesson's toolkit.
You now know how to tell legitimate CPU load apart from a pathological bug, and how to pin down exactly which method is responsible with dotnet-trace. Next up: the exact opposite signature — threads sitting frozen, CPU near zero, and nothing moving at all. Deadlocks, diagnosed live.
dotnetmadeeasy.com — Learn C# and .NET, the right way.