PM2 vs systemd: Which Process Manager for Node.js? Measured (2026)

PM2 vs systemd: Which Process Manager for Node.js? Measured (2026)

PM2 vs systemd is one of the first questions you run into the moment a Node.js app moves from your laptop to a server. Both keep your program running in the background, restart it after a crash and bring it back after a reboot. PM2 is a process manager from the Node world that you install with npm. systemd is the init system that already runs on almost every modern Linux machine. So the real question is not just “which one is better?” but also: do I need the extra layer at all?

We did not copy this from documentation. On 12 November 2026 we measured it on our own server: PM2 7.1.1, systemd 255 (Ubuntu 24.04), Node.js 22.23.5. The same tiny HTTP app ran once under PM2 and once as a systemd unit. We killed it with kill and kill -9, sent it into a crash loop, measured memory with smaps_rollup and fired 600 requests at it during a reload. Four findings up front:

1. PM2 restarts an app even after a normal kill (SIGTERM). systemd with Restart=on-failure does not, the service stays down. That is not a bug, it is a different idea of what “stopped” means.

2. In a crash loop, systemd gives up after 5 attempts in 2.6 seconds, PM2 after 15 restarts in about 5 seconds. Neither tells anyone unless you set it up.

3. The same app uses 12.1 MB of real memory (PSS) under PM2 and 8.3 MB under systemd. On top of that comes the PM2 daemon itself at 15 to 18 MB.

4. On our server a pm2-root.service had been running for months while managing zero processes, dragging along 131 MB of old log files. Nobody switched it off, because nothing was broken.

Two green humanoid robots facing each other in a blue-lit server room, holding a glowing orb between them

PM2 vs systemd: what is what?

Before measuring, a quick look at the terms, because the two tools do not quite play in the same league.

systemd is the first program Linux starts after the kernel. It has process ID 1 and brings up everything else: networking, SSH, databases, web servers and your own services. Each service is described by a small text file called a unit. We covered how to write one in detail in How to create a systemd service. systemd does not care about language: Node, Python, Go, a Bash script, it is all the same.

PM2 is a process manager written in Node.js itself. You install it with npm install -g pm2, start your app with pm2 start app.js and immediately get a nice table with status, CPU, memory and restart counter. PM2 runs its own background process, the so-called God Daemon, which starts and watches your apps as child processes. PM2 also ships features systemd does not have out of the box: a cluster mode that spreads several instances of your app across one port, and a zero-downtime reload.

The key point many tutorials skip: PM2 does not replace systemd. For PM2 to come back after a server reboot, the pm2 startup command creates a systemd unit that starts the PM2 daemon. This is what it looked like on our machine:

[Unit]
Description=PM2 process manager
After=network.target

[Service]
Type=forking
User=root
Environment=PM2_HOME=/root/.pm2
PIDFile=/root/.pm2/pm2.pid
Restart=on-failure
ExecStart=/usr/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/usr/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/usr/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

So the chain is always: systemd → PM2 daemon → your app. With plain systemd it is: systemd → your app. The honest comparison is therefore “systemd alone” versus “systemd plus PM2”.

Our test setup

To keep it fair, both runs used the exact same file. A mini app that listens on a port, returns its process ID and exits itself with code 1 via /crash:

const http = require('http');
const port = process.env.PORT || 4711;
http.createServer((req, res) => {
  if (req.url === '/crash') { res.end('bye'); setTimeout(() => process.exit(1), 10); return; }
  res.end('ok ' + process.pid + '\n');
}).listen(port);
process.on('SIGTERM', () => process.exit(0));

For PM2 we used a separate PM2_HOME under /tmp so the existing PM2 daemon on the server stayed untouched. For systemd we created a minimal unit with Restart=on-failure, which is what most guides tell you to use. After the tests we deleted both test environments completely.

We measured:

  • Restart behaviour on SIGTERM, SIGKILL and exit code 1
  • Crash loops: when does the manager give up?
  • Memory: not RSS but PSS from /proc/PID/smaps_rollup. Why RSS is misleading here is explained in How much RAM does a server need.
  • Downtime during restart and reload, measured with a loop of curl requests every 10 milliseconds
  • Start time of the PM2 daemon

Restart after a crash: the biggest difference

This is the difference that surprises people most in practice.

EventPM2 (default)systemd Restart=on-failure
kill (SIGTERM)restartedstays down
kill -9 (SIGKILL)restarted after 68 msrestarted after 379 ms
process.exit(1)restartedrestarted
Crash loopgives up after 15 restarts (~5 s)gives up after 5 restarts (2.6 s)

SIGTERM: PM2 treats any exit that did not come through pm2 stop as unplanned and starts the process again. systemd sees a clean exit with code 0 as intentional. So if you run a quick kill on an app on a systemd server, you have just switched it off. Under PM2 it is back a second later. Either can be right or wrong. If you want systemd to behave like PM2, set Restart=always.

SIGKILL: Both restart. PM2 was faster in our test (68 versus 379 milliseconds until the first successful response). The reason is simple: by default systemd waits RestartSec=100ms before restarting and then spawns a completely fresh Node process. For nearly every service a third of a second does not matter. If it does for you, lower RestartSec.

Crash loop: An app that crashes 200 milliseconds after start is not restarted forever by either tool. systemd steps in at its default limit (StartLimitBurst=5 within StartLimitIntervalSec=10s) and marks the service failed. PM2 stops after max_restarts (default 15) and shows errored. The problem is the same in both cases: no alarm goes off. The app is down and you notice when a user complains. With systemd, OnFailure= can start a unit that sends you a message. With PM2 you need a module or an third-party service. How we watch services in general is covered in Server monitoring.

Two blue robotic hands cradling a glowing translucent cube that is breaking apart into fragments

Memory usage: what the extra layer costs

PM2 shows a mem column in its table. For our test app it showed 29 MB right after start and 60.6 MB a second later. Both are RSS and not very useful for comparison, because shared libraries are fully counted for every process. The honest number is PSS, the share a process is actually responsible for:

ProcessRSSPSS (real share)
Test app under systemd47.8 MB8.3 MB
Test app under PM260.9 MB12.1 MB
PM2 daemon (God Daemon), our test64.2 MB15.3 MB
PM2 daemon on our server, managing zero apps66.6 MB18.2 MB
PM2 cluster with 4 instances + daemon–65.4 MB combined

Two things stand out. First, the same app runs with about 4 MB more under PM2, because PM2 wraps it in its own container script (ProcessContainerFork) that handles communication with the daemon. Second, the daemon itself costs another 15 to 18 MB, whether it manages one app or twenty. On a 1 GB VPS, 20 to 25 MB extra is noticeable. On a server with 8 GB or more it does not matter.

systemd itself does not appear in this table because it is running anyway. As a bonus, systemd tracks memory per service through cgroups: systemctl show -p MemoryCurrent servicename gave us 8.8 MB, very close to the PSS value. And with MemoryMax=200M in the unit you set a hard limit enforced by the kernel. PM2 has max_memory_restart, but that is an after-the-fact check: PM2 looks periodically and restarts the app if it has grown too large. The kernel limit under systemd applies immediately.

Cluster mode and zero downtime: PM2’s real strength

This is where PM2 plays its biggest card. One command starts several instances of your app sharing one port:

pm2 start app.js -i 4 --name my-app

By default Node.js uses one CPU core. PM2’s cluster mode uses Node’s built-in cluster module and spreads incoming connections across instances. In our test 8 requests landed perfectly evenly across the 4 processes, 2 each.

Then the real test: while a loop sent 600 requests 10 milliseconds apart, we ran pm2 reload. PM2 restarts one instance at a time and waits for the new one to be ready before killing the old one.

ActionRequestssucceededfailed
pm2 reload (cluster, 4 instances)6006000
pm2 restart (cluster, 4 instances)6006000
systemctl restart (single instance)3002964

Under systemd with a single instance, 4 of 300 requests were lost during the restart, about 1.3 percent, a window of a few dozen milliseconds in which nothing was listening on the port. For an internal API that rarely matters. For a shop checkout that is deployed constantly, it does.

To be fair: zero downtime works with systemd too, just not with one command. The usual approaches are two instances via a template unit (app@1.service, app@2.service) behind a reverse proxy such as nginx, restarted one after the other, or socket activation, where systemd holds the port. We did not measure those for this article, so there are no numbers for them here. It is more manual work, and that work is exactly what PM2 saves you.

Rows of illuminated server racks glowing with blue and purple neon light in a dark data center

Autostart after reboot: the pm2 save trap

Under systemd, autostart is one line: systemctl enable servicename. The unit file sits on disk and the service is back after every reboot.

Under PM2 it takes three steps, and the third one is forgotten all the time:

pm2 startup        # creates the systemd unit for the PM2 daemon
pm2 start app.js   # start the app
pm2 save           # write the current process list to ~/.pm2/dump.pm2

On boot the unit runs pm2 resurrect and starts everything listed in dump.pm2. We tested what happens without pm2 save: start app, kill daemon, run pm2 resurrect. Result: 0 processes. With pm2 save in between, the app came back cleanly.

This also means that with PM2, the state of your server lives in a file that is only current if you remember to update it after every change. Start a new app, forget pm2 save, and it is gone after the next kernel update. With systemd the unit file is the state. There is nothing to forget.

Logs: journald versus files that keep growing

systemd sends everything your app writes to stdout and stderr into the journal. You read it with journalctl -u servicename -f, filter by time (--since "1 hour ago") and never think about rotation, because journald has a cap. Ours is SystemMaxUse=1G; the journal currently uses 807.6 MB for the whole server and does not grow past the limit.

PM2 writes logs to files under ~/.pm2/logs/, two per app (out and error). By default without rotation. For that there is the pm2-logrotate module, which you have to install separately. We saw how quickly this goes wrong on our own server:

98 MB  nyx-gateway-error.log   (last written: 14 June 2026)
39 MB  nyx-gateway-out.log     (last written: 14 June 2026)
131 MB total in ~/.pm2/logs

A 98 megabyte error log for a service that has not run under PM2 since June. The files were never rotated and just sit there. If you use PM2, install pm2-logrotate on day one, or add ~/.pm2/logs/*.log to your normal rotation. We show how in logrotate setup. And if your disk is already full: Linux disk full.

A tiered stack of translucent glowing glass layers against a dark blue background

Our own finding: the daemon that does nothing

While preparing this article we checked what PM2 was actually doing on our server:

$ systemctl list-units 'pm2*'
pm2-root.service  loaded active running  PM2 process manager

$ cat ~/.pm2/dump.pm2
[]

The service is active, the daemon runs, starts on every reboot and uses 18 MB of real memory. And it manages nothing. The process list is empty. At some point in the summer we moved our Node services to plain systemd units (the server now runs about 90 services, all directly under systemd), but the PM2 daemon stayed behind.

This is not a disaster, but it is a typical pattern: a layer that no longer does anything goes unnoticed because it does not break anything either. systemctl status says “active (running)”, and that reads like “all good”. If you move from PM2 to systemd, clean up at the end:

pm2 unstartup systemd   # removes the pm2-<user>.service unit
pm2 kill                # stops the daemon
npm uninstall -g pm2    # optional

Other differences at a glance

CriterionPM2systemd
Installationnpm install -g pm2 (17 MB, 49 dependencies)already there
Languagesmainly Node.js, others via --interpreteranything
Usabilityvery beginner-friendly, pm2 ls, pm2 monitwrite a unit file, systemctl, journalctl
Restart on SIGTERMyesonly with Restart=always
Cluster / multiple instancesbuilt in (-i max)via template units, DIY
Zero-downtime reloadbuilt in (pm2 reload)via proxy or socket activation
Logsfiles, rotation via modulejournald, rotation built in
Resource limitsmax_memory_restart (after the fact)MemoryMax, CPUQuota (kernel)
Security / sandboxnone of its ownProtectSystem, NoNewPrivileges and more
Dependencies between servicesnoAfter=, Requires=, Wants=
Schedulingcron_restarttimer units
Start timedaemon needs ~290 ms coldinstant, already running
Autostartpm2 startup + pm2 savesystemctl enable

Security is often overlooked. systemd can lock a service down: read-only access to the system, no access to /home, no new privileges, a private /tmp. In our systemd article a test service went from 9.6 to 1.5 on the built-in security check that way. PM2 has nothing comparable. A typical PM2 setup also often runs as root, because pm2 startup uses the current user and many guides do everything as root. That was the case on our machine too (User=root in the generated unit). Under systemd, User=nodeapp is one line.

The systemd unit that replaces PM2 for a single app

If you want to move off PM2, this unit is enough for the vast majority of Node apps. It mimics PM2’s defaults (restart on any exit) and adds a few things PM2 cannot do:

[Unit]
Description=My Node app
After=network-online.target
Wants=network-online.target

[Service]
User=nodeapp
WorkingDirectory=/opt/my-app
ExecStart=/usr/bin/node /opt/my-app/server.js
Environment=NODE_ENV=production
EnvironmentFile=-/opt/my-app/.env
Restart=always
RestartSec=1
MemoryMax=300M
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/opt/my-app/data
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Then:

sudo systemctl daemon-reload
sudo systemctl enable --now my-app
journalctl -u my-app -f

Two pitfalls when switching: first, systemd does not know your shell environment. If you installed Node via nvm, systemd will not find node. Always use the absolute path from which node in ExecStart. Second, PM2 may have read an ecosystem.config.js with environment variables. Under systemd those go into Environment= or a .env file via EnvironmentFile=.

What if the app runs in Docker?

Then you usually need neither for the app itself. Docker restarts containers with restart: unless-stopped, and the Docker daemon is in turn started by systemd. PM2 inside a container (via pm2-runtime) only makes sense if you want cluster mode within one container. It is more common to run several container instances. More on containers in What is Docker, Docker vs Podman and our Docker Compose example.

A glowing fork at the end of a neon circuit-board path, symbolizing a junction

Our recommendation: PM2 or systemd?

Use systemd if …

  • you run individual services on your own Linux server (the normal case),
  • more than one language is involved, not just Node,
  • you need resource limits, sandboxing or dependencies between services,
  • you do not want to think about log rotation and pm2 save,
  • your server is short on RAM.

Use PM2 if …

  • you want to spread a Node app across several CPU cores without building a load balancer in front,
  • you deploy very often and need zero downtime without extra effort,
  • you are just starting out and pm2 ls and pm2 logs feel easier,
  • you work somewhere without root access and cannot create system units (although systemd supports user units too).

For us the answer is clear: all our own services run directly under systemd, about 90 of them. We never missed cluster mode, because our Node services are rarely CPU-bound and mostly wait on databases or third-party APIs. Where we really needed to scale, we would rather put two instances behind the reverse proxy. In the end PM2 did only one thing on our server: run empty.

This is not a verdict against PM2. It is a good tool with a very pleasant interface. But it is an extra layer on top of something that is already there, and every layer wants maintenance.

Frequently asked questions

What is the difference between PM2 and systemd?

systemd is the Linux init system and manages all services regardless of language. PM2 is a process manager written in Node.js, aimed mainly at Node apps, with extras such as cluster mode and zero-downtime reload. PM2 itself is started on boot by systemd.

Is PM2 better than systemd?

Not in general. PM2 is better at cluster mode and reloading without downtime. systemd is better at resource limits, security, logs and dependencies between services, and it uses no extra memory. For most individual services on a Linux server, systemd is enough.

Do you still need PM2?

For many setups, no. If you do not need cluster mode or zero-downtime deploys, systemd does everything PM2 does without any extra installation. PM2 remains handy if you want to spread a Node app across several cores.

Does systemd restart a Node app after a crash?

Yes, with Restart=on-failure it restarts on crashes and non-zero exit codes. If the app is stopped with a normal kill (SIGTERM), systemd does not restart it with that setting. If you want that, use Restart=always.

How much memory does PM2 use?

In our measurement the PM2 daemon used 15 to 18 MB of real memory (PSS), even with no app at all. An app under PM2 used about 4 MB more than the same app under systemd. The RSS figure in pm2 ls is much higher because shared libraries are counted in full.

What does pm2 startup do?

It creates a systemd unit (for example pm2-root.service) that starts the PM2 daemon on boot and restores the saved process list with pm2 resurrect. Without a subsequent pm2 save that list is empty and no app runs after a reboot.

Can systemd run multiple instances like PM2 cluster mode?

Yes, via template units such as app@.service, started as app@1, app@2 and so on, each on its own port. A reverse proxy such as nginx or Caddy then spreads the traffic. It is more manual than pm2 start -i 4 but gives you full control.

Should I use PM2 inside a Docker container?

Usually not. Docker restarts containers itself, and multiple container instances replace cluster mode. pm2-runtime in a container is only worth it if you explicitly want several Node processes in one container.

How do I migrate from PM2 to systemd?

Create a unit file per app (template above), start it with systemctl enable --now, verify it, then remove the app from PM2 (pm2 delete name), run pm2 save, and finally switch off the daemon with pm2 unstartup systemd and pm2 kill. Do not forget to clean up old log files in ~/.pm2/logs.

Does PM2 replace cron jobs?

PM2 can restart apps on a schedule with cron_restart, but it cannot schedule arbitrary tasks. For that you use classic cron jobs or systemd timers, which we show in the systemd article.

Conclusion

In the PM2 vs systemd comparison, neither wins across the board. PM2 has the nicer interface, built-in cluster mode and in our test got 600 of 600 requests through a reload. systemd is already there, needs no extra memory, can sandbox services, rotates logs on its own and has no state you can forget to save.

For a single Node app on your own server, systemd is the leaner choice. For a CPU-hungry Node app that should use every core and deploy without interruption, PM2 is convenient. And if you no longer need PM2: run cat ~/.pm2/dump.pm2 and check whether your daemon is still doing anything. Ours was not.

If you are just setting up a server, start with Linux server setup.