Skip to content

Profiling

Why Does Profiling Exist?

Metrics and tracing (from the Observability section) can tell you that a service is slow, and even which service in a call chain is responsible — but neither tells you why, at the level of actual code. A trace might show that a specific function call took 800ms; profiling exists to answer the next, more granular question: which specific lines of code, inside that function, are actually consuming that time — a loop that's doing unnecessary work, a call to an unindexed database query, an inefficient algorithm.

Metrics/Tracing tell you:  "the process_order() function took 800ms"

Profiling tells you:        "620ms of that 800ms was spent inside
                             THIS specific nested loop, on THIS
                             specific line"

This is a fundamentally more granular, code-level diagnostic tool than anything in the Observability section — it operates inside a single process, at the level of individual function calls and lines of code, rather than across services or requests.


CPU Profiling

The goal: determine exactly where a program is spending its CPU time, function by function, call by call.

Sampling Profilers

Periodically INTERRUPT the running program (e.g. every 10ms) and
record the CURRENT call stack

↓

Over thousands of samples, build up a STATISTICAL picture of where
time is being spent — a function that appears in 60% of samples is
likely consuming roughly 60% of the actual CPU time

Low overhead, since it's just periodically checking in rather than tracking every single function call — this is exactly why sampling profilers are the ones safe to run against real production traffic, where adding significant overhead would itself distort the very performance you're trying to measure (a form of the observer effect, covered below).

Instrumenting Profilers

Inject TIMING CODE at the start and end of every single function call

↓

Precisely, exactly measures how long every call took — but the
INSTRUMENTATION ITSELF adds real overhead to every call, which can
meaningfully distort the actual timing being measured

Much higher accuracy for a specific, controlled measurement, at the cost of overhead significant enough that it's typically reserved for a controlled development or testing environment — running this against live production traffic risks the profiling itself becoming a meaningful, confounding source of the very slowness you're investigating.


Flame Graphs — The Standard Visualization

A profiler's raw output (thousands of stack samples) is unreadable directly — flame graphs are the standard way to visualize it, and being able to read and explain one is a genuinely common, practical interview expectation.

                    ┌───────────────────────────┐
                    │      process_order()       │
                    └───────────────────────────┘
          ┌─────────────────┐    ┌───────────────────┐
          │  validate_input() │    │  save_to_database() │
          └─────────────────┘    └───────────────────┘
                                  ┌───────────────────┐
                                  │   db_query()        │
                                  └───────────────────┘
Width of a bar   =  proportion of TOTAL time spent in that function
                    (including everything it calls beneath it)

Stacked bars      =  the CALL STACK — a bar sitting on top of another
                     means it was called BY the function beneath it

The WIDEST bar at any given level is where the most time is going

Reading a flame graph is mostly about looking for the widest bars — they represent the functions actually consuming the most cumulative time, and are exactly where optimization effort should be focused first, rather than guessing based on which code merely looks complicated or unfamiliar.


Memory Profiling and Leak Detection

A separate but related concern: understanding what's actually consuming memory, and specifically catching memory leaks — objects that should have been garbage collected but are still being referenced somewhere, silently accumulating over time.

Take a HEAP SNAPSHOT at time T1

↓

Let the program run for a while, under normal load

↓

Take a SECOND heap snapshot at time T2

↓

DIFF the two snapshots — objects that have grown in count or
retained size between T1 and T2, without an obvious reason, are
the prime suspects for a leak

This diffing approach is exactly how real memory leak investigations proceed in practice — a single snapshot alone doesn't reveal a leak (everything looks "used" at any single point in time); it's the unexpected growth between two points, under otherwise stable load, that reveals something is accumulating when it shouldn't be.


Continuous Profiling — A More Modern Practice

Traditionally, profiling only happened reactively — once something was already known to be slow, an engineer would attach a profiler and try to reproduce the slow condition live. Continuous profiling flips this: run a very low-overhead sampling profiler on production services all the time, so historical profile data is already available to look back at exactly when a past incident happened.

Reactive profiling:    incident happens → engineer tries to
                       REPRODUCE the slow condition live, attaching
                       a profiler after the fact (sometimes the
                       condition doesn't reproduce easily, or has
                       already resolved by the time you look)

Continuous profiling:  profiling data was ALREADY being collected,
                       constantly, at low overhead — an engineer
                       can look BACK at exactly the time window the
                       incident occurred, without needing to
                       reproduce anything live at all

This is a genuinely valuable, increasingly common practice specifically because many real production slowdowns are difficult or impossible to reproduce on demand — having the profiling data already captured, continuously, removes the dependency on being able to recreate the exact conditions that caused the slowdown in the first place.


Real-World Examples

py-spy

A low-overhead sampling profiler for Python with a genuinely useful, specific property: it can attach to an already-running production process without needing to restart it, add any instrumentation code, or modify the running application at all — making it practical to profile a live incident in progress, not just a pre-instrumented test environment.


pprof (Go)

Go's built-in, first-class profiling support — CPU and memory profiling are treated as a core part of the language's standard tooling rather than an external add-on, reflecting how central profiling is considered to be for understanding a Go program's real-world performance.


Pyroscope / Parca (Continuous Profiling Platforms)

Purpose-built platforms specifically for the continuous profiling practice described above — collecting low-overhead profile data constantly across a fleet of production services, and providing the ability to look back at exactly the time window a past incident occurred, rather than needing to reproduce it live.


Common Interview Questions

How would you find out why a specific function or service is slow, beyond just knowing that it is?

By profiling it — using a sampling profiler to build a statistical picture of where CPU time is actually being spent, function by function, or a memory profiler to identify what's consuming memory or leaking over time. This is a more granular, code-level diagnostic than metrics or tracing alone provide, which can tell you a function is slow but not which specific lines inside it are responsible.


What's the difference between a sampling profiler and an instrumenting profiler, and why does production use typically favor sampling?

A sampling profiler periodically checks the current call stack, building a statistical picture with low overhead — safe to run in production without meaningfully distorting the very performance being measured. An instrumenting profiler injects precise timing code around every function call, giving exact measurements but at overhead significant enough that it's typically reserved for controlled development or testing environments, where that added overhead won't confound production traffic.


How do you find a memory leak?

By taking heap snapshots at two different points in time under otherwise stable load, then diffing them — objects that have unexpectedly grown in count or retained size between the two snapshots, without a clear legitimate reason, are the prime candidates for a leak. A single snapshot alone can't reveal this; it's the unexpected growth between two points that does.


What is continuous profiling, and why is it valuable?

Running a low-overhead sampling profiler on production services constantly, rather than only reactively after an incident is already known. It's valuable because many real production slowdowns are difficult or impossible to reproduce on demand — having profiling data already captured continuously means an engineer can look back at exactly the time window a past incident occurred, without needing to recreate the exact conditions live.


How do you read a flame graph?

The width of each bar represents the proportion of total time spent in that function (including everything it calls), and stacked bars represent the call stack — a bar on top of another was called by the one beneath it. The widest bars at any level represent where the most cumulative time is actually going, and are where optimization effort should be focused first.


Common Mistakes

❌ Using a high-overhead instrumenting profiler against live production traffic.

The instrumentation's own overhead can meaningfully distort the very timing being measured, or degrade real user-facing performance while profiling is active — sampling profilers are the appropriate choice for production use.


❌ Only profiling reactively, after an incident has already started.

The exact slow condition may be difficult or impossible to reproduce live by the time you're investigating — continuous profiling avoids this by having relevant historical data already captured.


❌ Trying to diagnose a memory leak from a single heap snapshot.

Everything looks "in use" at any single point in time — a leak is only revealed by comparing growth between two snapshots taken under otherwise stable conditions, not by inspecting one snapshot in isolation.


Key Takeaways

  • Profiling answers a more granular question than metrics or tracing can — not just that a function is slow, but which specific lines of code inside it are actually consuming the time or memory.
  • Sampling profilers, with low overhead, are safe for production use; instrumenting profilers give more precise measurements but at overhead usually reserved for controlled development or testing environments.
  • Flame graphs visualize profiler output as a call stack, with bar width representing cumulative time spent — the widest bars are where optimization effort should be focused first.
  • Memory leaks are found by diffing heap snapshots taken at two different points in time, not by inspecting a single snapshot alone, since unexpected growth between the two is what actually reveals a leak.
  • Continuous profiling — running a low-overhead profiler on production constantly — removes the dependency on reproducing a slow condition live, since historical profile data is already available to look back at exactly when a past incident occurred.