Best Practices

CPU profiling is a powerful but intricate part of optimizing Node.js application performance. While it is easy to get started with the basics, using profiling effectively requires a deeper understanding of the available tools, how they work, and what the results mean. This article introduces advanced CPU profiling techniques using Node.js’s built-in tools. You will learn how to create CPU profiles, troubleshoot common issues, manage and interpret profiling data, and adjust profiling arguments for different scenarios. With practical examples and detailed explanations throughout, you will gain the confidence you need to analyze and improve your application's performance.

How to create a CPU Profile

To create a CPU profile, you can use the --cpu-prof flag when running your Node.js application. This will generate a .cpuprofile file containing the profiling data.

Official docs: Node.js CPU Profiling

You can start CPU profiling in different ways

Direct command line flags

Using NODE_OPTIONS (Node.js v23.x+)

☝️ Note

The order of CPU profiling arguments is critical!

Bad:

Good:

Troubleshooting

What, of the many profiles, is the main process?

Based on the file naming conventions and the nature of process ID assignment, the main process always has the smallest Process ID (PID), and a Thread ID (TID) is 0 and appears at the end of an alphabetically sorted folder. The main process is the last file in the folder.

My profile files are appearing in different places in the file system based on the cwd of every process

This is because of the way Node.js handles the --cpu-prof flag. It will use the CWD of the process to determine the location of the profile file. To avoid this, you can use --cpu-prof-dir with an absolute path.

Now all of them are in one place:

Error: --cpu-prof is not allowed in NODE_OPTIONS

Node.js maintains a security whitelist for NODE_OPTIONS. CPU profiling flags were historically blocked for security reasons.

Version Support

  • ≤ v22.x: All CPU profiling flags blocked in NODE_OPTIONS

  • ≥ v23.x: All CPU profiling flags allowed (PR #57018)

Test your Node.js version support

Workarounds for v22.x and earlier

CPU Profile Filename

Date and Time

The date and time in the filename represent the wall-clock write time, which is when the profile was flushed. This timestamp generally corresponds to the process creation time, not the moment the file was written to the file system.

Process and Thread IDs

The CPU profile filename includes both a Process ID (PID) and a Thread ID (TID). Understanding how these IDs are generated is crucial for interpreting profile files, especially in applications involving multiple processes or worker threads.

A Node.js application starts as a single process. This main process can then spawn child processes or create worker threads. Each new process and worker thread is assigned a distinct ID, which is reflected in the CPU profile filenames. Node.js provides built-in mechanisms for accessing these identifiers. For instance, process.pid gives the Process ID. The Thread ID for worker threads can be obtained using worker_threads.threadId. In profiling contexts, the main thread is conventionally identified as 0, as it doesn't have a threadId property like worker threads do.

The following script demonstrates how to retrieve these values:

Output:

What Determines the Process ID (PID)?

The PID represents the OS-level process ID.

A new PID is generated each time a new process is created. Common ways to create new processes in Node.js include:

  • child_process.fork()

  • child_process.spawn()

  • child_process.exec()

  • Directly running the node executable (e.g., starting a new script).

Consider the following script that spawns two child processes:

Output (order of spawn children may vary slightly due to asynchronous nature):

☝️ Note: The process ID process.pid is assigned incrementally. The initial process is always the smallest PID and TID is 0.

If --cpu-prof is added to the command (e.g., node --cpu-prof script.js), CPU profiles are generated for the parent process and each child process, distinguishable by their PIDs in the filenames:

  • CPU.<timestamp>.51430.0.001.cpuprofile (Parent process)

  • CPU.<timestamp>.51431.0.002.cpuprofile (First child process)

  • CPU.<timestamp>.51432.0.003.cpuprofile (Second child process)

What Determines the Thread ID (TID)?

The TID represents V8's internal thread identifier.

By default, Node.js applications run in a single main thread, which typically has a TID of 0. When a Worker thread is created using the node:worker_threads module, it is assigned a new, unique TID.

The following script creates two worker threads:

Output (order of worker messages may vary):

If --cpu-prof is used with this script, profiles are generated for the main thread and each worker thread, distinguished by their TIDs (and potentially sequence numbers if they exit around the same time):

  • CPU.<timestamp>.51430.0.001.cpuprofile (Main thread)

  • CPU.<timestamp>.51430.1.002.cpuprofile (Worker 1)

  • CPU.<timestamp>.51430.2.003.cpuprofile (Worker 2)

Sequence number

The sequence number (.001, .002, etc.) in the filename is incremented for each profile generated during the same execution, ensuring uniqueness even if multiple treads are profiled, in the same process. Note, this is NOT A GLOBAL UNIQUE ID. It is only unique for the current process.

CPU Profiling Arguments

☝️ Note: The "Added in" versions indicate when these flags were introduced. Some flags were initially experimental. Always consult the official Node.js documentation for the most current information on their status and behavior in your Node.js version.

Flag

Added in

Default

Description

--cpu-prof

v12.0.0 (Exp)

off

Starts the V8 CPU profiler on startup and writes a .cpuprofile on exit.

--cpu-prof-dir

v12.0.0 (Exp)

Current working directory. If --diagnostic-dir is set, it defaults to that directory.

Directory where --cpu-prof outputs are written.

--cpu-prof-name

v12.0.0 (Exp)

CPU.${yyyymmdd}.${hhmmss}.${pid}.${tid}.${seq}.cpuprofile

Filename to use for the CPU profile.

--cpu-prof-interval

v12.2.0 (Exp)

1000 (microseconds/µs)

Sampling interval in microseconds for the CPU profiler.

--cpu-prof

  • Purpose: Starts the V8 CPU profiler on application startup. When the Node.js process exits, a .cpuprofile file containing the profiling data is written.

  • Default: off

  • Usage Example:

This command will profile and generate a profile file (e.g., CPU.<timestamp>.<pid>.<tid>.<sequence>.cpuprofile) in the current working directory upon exit.

--cpu-prof-dir

  • Purpose: Specifies the directory where CPU profile files generated by --cpu-prof will be saved.

  • Default: The current working directory. If --diagnostic-dir is set, it defaults to that directory instead.

  • Usage Example:

This will save the CPU profile into the ./profiles subdirectory (relative to the current working directory). If the directory doesn't exist, Node.js will attempt to create it.

  • Notes:

    • If --cpu-prof-name specifies an absolute path, --cpu-prof-dir is ignored.

    • Ensure the specified directory is writable by the Node.js process.

--cpu-prof-name

  • Purpose: Specifies a custom filename for the generated CPU profile.

  • Default: CPU.<yyyymmdd>.<hhmmss>.<pid>.<tid>.<seq>.cpuprofile (see CPU Profile Filename for details on the pattern).

  • Usage Example:

This will generate a profile file named my-custom-profile.cpuprofile. If --cpu-prof-dir is also specified, the file will be placed in that directory; otherwise, it will be in the current working directory.

  • Notes:

    • You CANNOT use placeholders for filenames (e.g., ${pid}, ${timestamp}) in your custom name.

    • If an absolute path is provided (e.g., /tmp/my-custom-profile.cpuprofile), the file WILL NOT be saved there.

--cpu-prof-interval

  • Purpose: Sets the sampling interval for the CPU profiler in microseconds (µs). This determines how frequently the profiler captures the state of the JavaScript call stack.

  • Default: 1000 (microseconds/µs), which is 1 millisecond.

  • Usage Example:

This command profiles an inline script (console.log('CPU')) with a sampling interval of 100 microseconds.

How to use intervals?

The --cpu-prof-interval flag specifies the sampling frequency in microseconds.

  • A smaller interval (e.g., 50 µs, 100 µs) collects more samples, providing a highly detailed profile. This is useful for short-running processes or identifying fine-grained bottlenecks. However, it results in larger .cpuprofile files and can add slightly more overhead to the profiled application.

  • A larger interval (e.g., 1000 µs, 5000 µs) collects fewer samples, resulting in smaller profile files and reduced overhead. This is suitable for longer-running processes or when a high-level overview of CPU usage is sufficient.

Start with the default (1000 µs) and adjust based on the granularity of data you need and the acceptable size of the generated profile files. The following section includes a table that illustrates the impact of different intervals on a sample application.

Illustrative Example: Impact of Interval Choice

This example demonstrates how different --cpu-prof-interval values affect the resulting CPU profile files. We'll profile a simple script with various intervals and analyze the differences in detail, file size, and overhead.

Step 1: Generate Profiles with Different Intervals

Run the following command to create profiles with intervals ranging from 1μs to 10,000μs:

Step 2: Analyze the Generated Profiles

Use this command to compare the profiles and display their characteristics:

Step 3: Results Analysis

The table below shows how the sampling interval affects profile characteristics:

Profile File

Sampling Interval

Duration (Script)

Total Nodes

Total Samples

File Size

Visual Example

empty-1.cpuprofile

1 µs

58.5 ms

472

10,514

162.0 KB

cpu-profile-interval-1

empty-10.cpuprofile

10 µs

17.5 ms

338

931

73.3 KB

cpu-profile-interval-10

empty-50.cpuprofile

50 µs

16.0 ms

245

312

45.2 KB

cpu-profile-interval-50

empty-100.cpuprofile

100 µs

15.0 ms

153

100

28.9 KB

cpu-profile-interval-100

empty-1000.cpuprofile

1000 µs (1 ms)

13.9 ms

49

10

8.6 KB

cpu-profile-interval-1000

empty-2000.cpuprofile

2000 µs (2 ms)

14.2 ms

32

7

5.8 KB

cpu-profile-interval-2000

empty-4000.cpuprofile

4000 µs (4 ms)

13.5 ms

18

4

3.2 KB

cpu-profile-interval-4000

empty-5000.cpuprofile

5000 µs (5 ms)

14.8 ms

15

3

2.7 KB

cpu-profile-interval-5000

empty-7000.cpuprofile

7000 µs (7 ms)

13.2 ms

9

2

1.8 KB

cpu-profile-interval-7000

empty-10000.cpuprofile

10000 µs (10 ms)

14.0 ms

3

2

0.5 KB

cpu-profile-interval-10000

Examples of Combining Arguments

1. Experiment Comparison

Organized profiling for A/B testing or feature comparison experiments.

2. Production Monitoring

Low-overhead profiling with reduced sampling frequency.

3. Performance Benchmarking

High-detail profiling with frequent sampling for bottleneck analysis.

4. CI/CD Performance Testing

Automated profiling in build pipelines using NODE_OPTIONS (Node.js v23.x+).

  • This works for spawning both child processes and worker threads.

  • All profiles end up in the same directory due to the absolute path.

  • The sampling interval is very high to enable long-running processes to be profiled.

5. Handling options with .env

Conclusion

Advanced CPU profiling is essential for keeping Node.js applications running smoothly as they grow. By applying these techniques, you can find and fix performance issues before they escalate into larger problems that affect users. As you gain experience with these tools, you will find that even the most complex optimization challenges become more approachable.