- Section
- performance
- Kind
- Editorial
- Reading time
- 4 min
- Updated
- 8 September 2026
Profile before you optimize: reading a flame graph without guessing
Misinterpreting a flame graph is easy. The x-axis is not time, the boxes represent sample proportion, and to make it worse, your sampling conditions affect what you see. This article will show you the right way to approach a flame graph, capture any sample accurately, and compare results with confidence.
How to read the shape
Width shows sample population, not time.
A flame graph shows the stack sampled, not how it changes over time. The y-axis represents stack depth, not time. The width is the overall proportion of the stack in that state, against all observed states, sorted alphabetically, not by time as you read the picture left to right.
[]
When examining a single frame, its width reflects the total CPU time spent executing that frame, its ancestors, and all children.[1] If one frame consumes more CPU time, its box will be wider. But more frequent execution also makes a function's box wider. Hence, a wide frame is not proof of an expensive function. It could also just get called a lot.
Self time versus total time
Self time, also called CPU time, is the amount of time that a function spends in execution, measured excluding the time cost of functions it calls that the original function calls. Self time versus total time provides insight into CPU utilization and can indicate if some functions are used excessively, but a flame graph's useful to focus on overall width, which is more robust.[2] When reading a flame graph, you are looking at the widest boxes, which are the ones most likely to be the biggest offenders for overall performance, but bear in mind that this is just a proportion, not a proof. You have not yet proven it.
Why samples can mislead
Frequent sampling is not always better.
Sampling many times a second does aggregate the call stacks into a more complete picture, but it comes at a cost. More samples mean more overhead, more chance of perturbing the system and getting false hotspots created by the profiling tool itself. When adding samples, the proportion you boosted for the first few steps starts to return to diminishing returns later on. Remember that your samples are not the whole truth, just a fraction. But a small fraction at high sampling rates can skew what you normally see to an unreliably high number, whatever the truth of what both functions add up to. Add to that the shortness of short frames, which are over-sampled even at a high frequency, and you are looking at a limited picture.
Sampling longer is often better than more often.
A long time for profiling, but not so long that it will wait for a rarely-encountered external dependency, this helps ensure the samples you study will follow the breadcrumbs to the root cause, not just finger a choke point downstream. When the side effects of sampling overhead are on you, minimizing them means sampling over a longer period with fewer samples, not more.[3]
What a sane profiling session looks like
In terms of sampling, there are a few general rules. [4] Default to a safe sampling rate 49 Hz or 99 Hz. Set sampling to run for a safe duration, which is substantial without overwhelming you. For the captured self time to be a reasonable stand-in for real time, give your sample sufficient time to accumulate, depending on the kind of profile. For a better profile, make sure it hits all the kinds of performance behavior it should, so that you are not just seeing a fraction that may not be representative. A differential audio flame graph can record a flame graph of the program in the 'before' and 'after' states for comparison. [5]
Remember when you are using the two profiles to compare, you are not just trying to measure a speed-up, you are trying to see cumulative change, to see that the same kind of profile is more space-efficient the second time round. A differential flame graph can compare two snapshots reliably. You are using the same kind of sample at each profile, and this lets you track execution. This means that the two samples you are comparing are the same kind of sample. You are not trying to guess a regime change from comparing the numbers you have to hand — these are not 'before' and 'after', but the two samples you actually have to work with when you are optimising.
Comparing two profiles
Comparing profiles in the same state is especially important, because the visual result is only meaningful in that light. Compare profiles as they are, not as an ideal represents them. Without that, you are throwing away information about the consistency of these profiles when comparing them.
Don't forget the big picture
So you got your optimisation to show up on the flamegraph as a change in width — what then? What is your next burn, after upping the sampling rate?
If you find that the next phase is tiny, then be wary. When your performance optimisation is small, and the next phase of your flamegraph suggests the next phase is smaller still, that is your signal to stop. At that point, chasing it is worthless; the answer is to level up your profiling to find some hotspots, and then attack them in turn. At this stage, you are just executing code and measuring it; you are not arriving at an opinion, or seeing varying breadths of truth. When you're working with the flame graph, the thing to remember, is that you are equal in reading it as you are in understanding it. You and the output are partners, so a dialogue.