PM2 cluster mode: app restarts "exited with code [0] via signal [SIGABRT]" and its error log is empty — the "JavaScript heap out of memory" text is in $PM2_HOME/pm2.log
In PM2 cluster mode, a Node worker's file descriptors 1 and 2 point at the PM2 daemon's own log ($PM2_HOME/pm2.log), not at the app's out/error files. PM2 captures console output through the cluster IPC, but native writes don't go that way, and V8's fatal heap-OOM report is one of them. So the app's error_file shows nothing, and pm2.log has "FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory" followed by "exited with code [0] via signal [SIGABRT]". Look in pm2.log first when a cluster app restarts on SIGABRT.
What happened
A Next.js server run by PM2 in exec_mode: "cluster" (1 instance, node_args: ["--max-old-space-size=512"]) restarted several times a day. PM2's log said only:
App [web:4] exited with code [0] via signal [SIGABRT]
The app's own error log (error_file) had nothing at those times, no stack trace and no message. It looked like a native crash.
Where the message was
The worker's stdio file descriptors point at the daemon's log:
$ ls -l /proc/<worker pid>/fd/1 /proc/<worker pid>/fd/2
/proc/<pid>/fd/1 -> $PM2_HOME/pm2.log
/proc/<pid>/fd/2 -> $PM2_HOME/pm2.log
In cluster mode, PM2 delivers the app's console/process.stdout output to the out/error files over the cluster IPC. Text written straight to fd 2 from native code bypasses that. V8's fatal-error handler is one such writer. So the explanation was sitting in pm2.log, just before each exited ... SIGABRT line:
<--- Last few GCs --->
... Mark-Compact 507.4 (515.0) -> 503.9 (515.1) MB ... allocation failure; scavenge might not succeed
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory
----- Native stack trace -----
1: ... node::OOMErrorHandler(char const*, v8::OOMDetails const&)
That is an ordinary heap exhaustion at the --max-old-space-size cap, not a native bug.
For contrast, a fork-mode app on the same host has fd 2 as a socket to the daemon, so its native stderr does reach its own error file.
How to check yours
grep -c 'JavaScript heap out of memory' "${PM2_HOME:-$HOME/.pm2}/pm2.log"
grep -B30 'via signal \[SIGABRT\]' "${PM2_HOME:-$HOME/.pm2}/pm2.log" | grep -E 'FATAL|Mark-Compact'
Mark-Compact numbers right under your --max-old-space-size mean the heap cap is being hit. Then measure what fills it, or raise the cap while staying under max_memory_restart.
Side effect worth knowing (Ubuntu)
With apport as the core handler (/proc/sys/kernel/core_pattern is a pipe to apport), each abort also wrote a core dump of about 1.5 GB to /var/lib/apport/coredump/ (the five newest kept, about 7 GB). The process stays alive and unresponsive while the dump is written, a few seconds here, which lengthens each outage before PM2 restarts it.
Conditions
- pm2
- 6.0.14
- node
- 24.19.0
- os
- Ubuntu 24.04.5 LTS
- exec_mode
- cluster
- app
- Next.js 16.3.8 server
- observed
- 2026-10-08