Learn how to read a flame graph.

•

•

Reading a Flame Graph Without Guessing

I’ve lost count of how many times I’ve sat through “performance tuning” workshops where some guy in a suit tries to sell you a massive observability suite before even explaining how to read a flame graph. They make it sound like some arcane, mystical ritual that requires a PhD in computer architecture, when in reality, it’s just a way to see where your CPU is spending its life. I didn’t learn this from a polished slide deck or a paid certification; I learned it while staring at a terminal in a dark room, trying to figure out why our payment gateway was suddenly choking on a Tuesday morning.

I’m not here to give you a theoretical lecture or a list of every tool available on GitHub. Instead, I want to show you what I actually look for when the latency spikes and the alerts start screaming. I’ll walk you through the patterns that actually matter—the wide bars, the deep stacks, and the silent killers that hide in plain sight. By the end of this, you won’t just know the mechanics; you’ll know how to stop guessing and start actually seeing why your code is melting.

Table of Contents

Mastering Cpu Profiling Visualization Under Pressure

Mastering Cpu Profiling Visualization Under Pressure.

When the alerts start firing and the latency numbers are climbing, your brain naturally wants to go into a panic. You start guessing. You think, “It must be the database,” or “Maybe it’s the garbage collector.” This is where most people fail. To actually solve the problem, you have to stop guessing and start looking at the call stack analysis provided by your profiler. A flame graph isn’t just a pretty picture; it’s a map of where your CPU is actually spending its life. When you’re looking at the visualization, ignore the noise. Don’t get distracted by the deep, narrow spikes that represent trivial utility functions. Instead, look for the wide plateaus. Those wide tops are where your code is sitting and burning cycles, and that is where you’ll find your culprit.

You also need to be careful about what kind of data you’re actually looking at. I’ve wasted hours chasing ghosts because I didn’t realize I was looking at the wrong thing. There is a massive difference between on-cpu vs off-cpu flame graphs. If your service is waiting on a lock or a network call, an on-cpu profile will show you nothing but silence or misleadingly small stacks. If the CPU isn’t doing the work, you won’t find the bottleneck by looking at execution time alone. You have to know if you’re hunting for a computation problem or a waiting problem before you even open the tool.

Turning Stack Trace Visualization Into Actionable Data

Turning Stack Trace Visualization Into Actionable Data

Once you have the graph open, the temptation is to just look for the widest bars and call it a day. That’s a mistake. A wide bar tells you that a specific function is responsible for a lot of time, but it doesn’t tell you why. You need to perform a proper call stack analysis to see if that function is actually doing heavy lifting or if it’s just a victim of how it’s being called. I’ve spent way too many hours chasing “hot” functions only to realize they were just the entry point for a massive, inefficient loop happening three layers deeper in the stack.

The real trick is distinguishing between where the CPU is actually working and where it’s just waiting around. If you are looking at standard CPU profiling visualization, you are seeing the “on-cpu” time—the actual computation. But if your service is sluggish because it’s waiting on a database lock or a slow disk I/O, a standard flame graph might leave you staring at a blank screen. This is where understanding on-cpu vs off-cpu flame graphs becomes the difference between a quick fix and a three-hour investigation. You have to know if you’re looking for a math problem or a waiting problem.

Five things I wish I knew before the next outage

  • Ignore the width, look for the plateaus. A single wide bar is interesting, but a series of wide bars stacked on top of each other—a plateau—is usually where your actual logic is eating your CPU cycles. If the bars are all skinny and fragmented, you aren’t looking at a heavy computation; you’re likely looking at massive context switching or thread contention.
  • Stop hunting for “the” function. You’re rarely looking for one single bad line of code. Usually, you’re looking for a pattern of calls that, when combined, represent a systemic misuse of a resource. I used to look for the widest bar; now I look for the branch of the tree that keeps getting wider as it goes down.
  • Watch out for the “sampling bias” trap. Remember that flame graphs are statistical snapshots, not a perfect trace of every instruction. If a function is extremely fast but called a billion times, it might show up differently than a slow function called once. Don’t mistake a statistical quirk for a smoking gun without checking your sampling frequency.
  • Context is everything. A function that looks expensive in isolation might just be a victim of its caller. If you see a massive block of time spent in a utility library, don’t go blaming the library authors. Trace it back up the stack to see which part of our code is calling it like a maniac.
  • Use the “Differential” approach when you can. Comparing one flame graph to another is ten times more useful than staring at a single one. If you have a profile from a healthy state and one from the outage, the delta tells you exactly what changed. It turns a guessing game into a search for a specific delta.

What I actually carry away from this

Stop looking for a single “smoking gun” line of code; look for the widest plateaus in the graph. If a function is taking up 40% of the width, that’s where your money is being wasted, regardless of how “elegant” the logic looks.

A flame graph is a map of where time is being spent, not a sequence of events. Don’t mistake the vertical stack for a timeline; the width tells you the cost, and the depth tells you how you got there.

Use profiling to validate your suspicions, not to form them. If you go into a profiling session guessing, you’ll find what you’re looking for even if it’s not the actual bottleneck. Start with a hypothesis, then let the graph prove you wrong.

Moving Beyond the Hype

Moving Beyond the Hype: Flame Graph Analysis

At the end of the day, a flame graph isn’t some magical lens that reveals the “truth” of your code; it’s just a way to stop guessing and start looking at where the CPU is actually spending its time. We’ve covered how to spot the wide plateaus that represent your biggest bottlenecks and how to navigate the stack traces without getting lost in the noise of library internals. The goal isn’t to achieve some perfect, theoretical optimization, but to identify the specific, heavy functions that are actually driving your latency. If you can look at a profile and say, “That wide block right there is why our p99 is spiking,” then you’ve already won half the battle.

Don’t let the complexity of the visualization intimidate you. I’ve spent far too many nights feeling like I was staring at a Rorschach test instead of a performance profile, only to realize I just hadn’t learned how to read the shapes yet. Profiling is a skill that you build through repetition and, more often than not, through the frustration of being wrong. Use these tools when things are breaking, use them when things are slow, and most importantly, use them to write better code next time so you don’t have to pull the flame graph out again at 3 a.m.

Frequently Asked Questions

How do I know if the spikes I'm seeing are actually a problem or just normal background noise from the runtime?

The short answer is: you don’t know until you compare it to a baseline. I’ve spent way too many hours chasing “ghost” spikes that turned out to be nothing more than the JVM’s garbage collector doing its job or a scheduled cron job. My rule of thumb is to capture a profile during a known “quiet” period. If the spike doesn’t show up in the baseline, it’s a signal. If it does, it’s just the cost of doing business.

Is there a way to tell if the bottleneck is my actual code or just the overhead of the profiler itself messing with the results?

It’s a valid concern. Profilers aren’t free; they inject overhead that can skew your results, especially in high-frequency loops. I usually check this by running a baseline with a “no-op” function or a very simple, known workload. If the flame graph shows massive spikes in `perf` or instrumentation symbols rather than your logic, the profiler is lying to you. If the overhead is too high, I switch to sampling-based profiling—it’s less intrusive and much more honest.

What's the difference between a flame graph and a regular call tree, and why would I bother with the visual one?

A call tree is a hierarchy; it tells you exactly what called what, but it’s a nightmare to read when you have ten thousand functions. It’s a list of paths. A flame graph collapses that noise. It doesn’t show the sequence, it shows the weight. It lets me ignore the trivial stuff and instantly see which specific stack traces are actually eating my CPU cycles. I don’t need a map of every street; I need to see where the traffic jam is.

How often should I actually be running these profiles—is once a week enough, or am I missing things by not doing it constantly?

If you’re running profiles once a week, you’re essentially taking a snapshot of a moving target. It’s fine for spotting slow regressions, but it won’t catch the transient spikes that actually wake you up at 2 a.m. I prefer continuous profiling in staging, or at least keeping a low-overhead profiler running in production. You don’t need constant data, but you do need the ability to look back at the exact moment the latency spiked.

Tomasz Wierzbicki

About Tomasz Wierzbicki

I am not interested in arguing about frameworks. I am interested in why the thing broke at two in the morning and what would have prevented it. Most of what I know came from being wrong in production and writing it down afterwards.