NodeJSProfiling Node.js Apps

Profiling Node.js Apps

A profiler measures where your program actually spends its time, so you fix the real bottleneck instead of guessing. Node has profiling built into V8: a sampling CPU profiler that records which functions are on the stack thousands of times per second, plus tooling to render the result as a flame graph. This page covers the quick --prof flag, the richer --cpu-prof that feeds Chrome DevTools, reading flame graphs, the 0x tool, and the difference between sampling and instrumentation profilers.

The quickest profile: --prof

Bash
# 1. Run with the V8 profiler — writes isolate-*.log (raw tick data):
node --prof app.js

# 2. Process the log into a human-readable summary:
node --prof-process isolate-*.log > profile.txt
 [Summary]:
   ticks  total  nonlib   name
   8412   72.1%   91.3%  JavaScript
    520    4.5%    5.6%  C++
   2901   24.9%          GC

 [Bottom up (heavy) profile]:
  ticks parent  name
  6210  53.2%   LazyCompile: *hashPassword bcrypt.js:42
  1804  15.5%   LazyCompile: *parseBody body-parser.js:88
`--prof` is a sampling profiler — it records the call stack thousands of times per second
V8's profiler is a **sampling** profiler: many times per second it records what function is currently executing (the top of the stack). Functions that show up in many samples are where the CPU time goes. `--prof` writes a raw tick log that `--prof-process` turns into a readable report — the "Bottom up (heavy) profile" ranks functions by how much time was spent *in them*, pointing straight at hot spots. In the example above, `hashPassword` dominates at 53% — a clear, synchronous CPU cost to investigate. A high `GC` percentage signals excess allocation worth reducing.
Richer profiles for DevTools: --cpu-prof

Bash
# Writes a .cpuprofile you can load into Chrome DevTools' Performance panel:
node --cpu-prof --cpu-prof-dir=./profiles app.js
# → ./profiles/CPU.20260621.<pid>.cpuprofile  (drag into DevTools → Performance)

Profile only a specific section programmatically

JS
import inspector from 'node:inspector'
import fs from 'node:fs'

const session = new inspector.Session()
session.connect()
session.post('Profiler.enable', () => {
  session.post('Profiler.start', () => {
    doExpensiveWork()                           // the code you want to profile
    session.post('Profiler.stop', (err, { profile }) => {
      fs.writeFileSync('section.cpuprofile', JSON.stringify(profile))
    })
  })
})
`--cpu-prof` gives a visual flame chart in DevTools — far easier to read than the text report
While `--prof` gives a text summary, `--cpu-prof` produces a `.cpuprofile` you load into [Chrome DevTools](/nodejs/chrome-devtools)' Performance panel for an interactive **flame chart** — the most intuitive way to see where time goes. For surgical analysis, the `inspector` module lets you start and stop the profiler programmatically around a specific operation, so the profile contains *only* the code you care about instead of the whole process lifetime. This targeted approach gives a much cleaner signal when you already suspect a particular function.
Reading a flame graph

Text
           ┌──────────────────────────────────────────────┐
  width =  │              handleRequest  (100%)             │
  time     ├───────────────────────┬──────────────────────┤
  spent    │  queryDatabase (30%)  │  renderResponse (65%) │  ← widest = hottest
           ├──────────┬────────────┤  ┌───────────────────┤
  stack    │ parse(8) │ serialize  │  │ hashPassword (60%)│  ← the real cost
  depth ▼  └──────────┴────────────┘  └───────────────────┘
  (y-axis = call stack; x-axis = time, NOT chronological order)
In a flame graph, WIDTH is time and a wide synchronous bar is a blocked event loop
Read flame graphs by **width, not height**: the x-axis is total time spent, the y-axis is just call-stack depth (who called whom). A wide bar is an expensive function; a wide bar *with no async gaps beneath it* is synchronous work hogging the [single thread](/nodejs/event-loop) — the cardinal Node performance sin, because it stalls every concurrent request. Look for the widest plateaus and trace them down to the leaf function actually burning the CPU. Note the x-axis is **not chronological** — bars are aggregated by function, not ordered by time. Narrow spikes rarely matter; chase the wide ones.
Flame graphs with 0x

Bash
npx 0x app.js
# Exercise the app (run load against it), then Ctrl-C.
# 0x generates an interactive flame-graph HTML file and opens it.

# Or against a load test:
npx 0x -- node app.js   # then run autocannon/k6 at it, stop, view the graph
`0x` produces a zoomable, searchable flame graph in one command — no manual log processing
`0x` is a popular zero-install tool (`npx 0x app.js`) that wraps the V8 profiler and renders a polished, interactive flame graph you can zoom, search, and filter in the browser — no manual `--prof-process` step. The workflow: start your app under `0x`, drive realistic traffic at it with a [load tester](/nodejs/load-testing) (because a profile is only meaningful under representative load), stop, and explore the graph. It's the fastest path from "something's slow" to a visual map of where the CPU went.
Sampling vs instrumentation profilers

Sampling (V8, --prof, 0x)

Instrumentation

How

Periodically records the call stack

Wraps every function with timing hooks

Overhead

Low — safe for near-production loads

High — can distort the very timings it measures

Accuracy

Statistical — misses very short, rare calls

Exact call counts and durations

Best for

Finding hot paths in real workloads

Precise call-count analysis of a small area

Sampling profilers are the default because their low overhead doesn't distort results
Node's built-in profiler is a **sampling** profiler, and that's usually what you want: its low overhead means the profile reflects real behaviour rather than the profiler's own cost. **Instrumentation** profilers (which time every function call) give exact counts but their overhead can be so high it skews results — making a fast function look slow simply because of the measurement wrapping. For finding where a server spends time under load, sampling is the right tool; reserve instrumentation for precisely counting calls in a narrow, suspected area. Profile under **realistic load** either way — an idle process tells you nothing.
Next
Hunt down memory that's never released: [Finding Memory Leaks](/nodejs/memory-leaks).