Every Node.js app that survives more than a week in production eventually asks the same question: what keeps it running when it crashes, and what brings it back when the server reboots? Two answers dominate - PM2, a Node-native process manager, and systemd, the init system already living inside virtually every modern Linux distribution.

They are not competing at exactly the same layer. PM2 is a userspace daemon you install on top of the OS. systemd is the OS. Understanding where the boundary sits is the difference between a setup that quietly works for years and one that fights you every time you deploy. This guide walks through the real, documentation-backed behavior of each so you can pick deliberately.

What each one actually is

PM2 is a daemon process manager written for Node.js, installed via npm. You start it with npm install pm2@latest -g, then launch an app with pm2 start app.js. PM2 daemonizes the process, keeps it alive, and gives you a CLI to inspect status, tail logs, and reload the app.[1] Its headline feature for Node is cluster mode: with the -i flag it spans a workload across every available CPU with zero changes to your code.[2]

systemd is the service manager and init system used by Ubuntu, Debian, Arch, Fedora, and most other major distributions. It supervises every service on the machine - network, cron equivalents, databases, you name it - via unit files. Your Node.js app is just one more .service unit it manages with the same machinery that keeps the whole OS alive.

The practical consequence: you do not need to install anything to use systemd, and it runs your process with the same tooling and hardening used for the rest of the system.

Starting the app and knowing it’s actually up

The trivial case for PM2 is a single command: pm2 start app.js.[1]

npm install pm2@latest -g
pm2 start app.js --name my-api

With systemd, you write a unit file, place it in /etc/systemd/system/, and tell systemd to start and enable it:

[Unit]
Description=My Node.js API

[Service]
Type=exec
User=www
WorkingDirectory=/srv/my-api
ExecStart=/usr/bin/node /srv/my-api/server.js
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now my-api.service

The differences here matter once you start caring about truthful readiness. systemd’s Type= controls how the manager decides the unit has finished starting. Type=simple is the default, but systemd’s own documentation recommends Type=exec, which only considers the unit started after the service binary has actually been executed - so systemctl start reports failure when the binary is missing or the user doesn’t exist, instead of falsely reporting success.[3] That is a genuinely useful correctness guarantee you get for free.

PM2 reports the app as “online” from its own daemon’s perspective. That is usually enough, but if you rely on readiness signals for orchestration, note that PM2’s view and the app’s real state can diverge unless you layer on your own health checks.

Auto-start at boot

A process manager that disappears on reboot is worse than none. This is where the two philosophies diverge sharply.

PM2 does not survive a reboot on its own. You must generate a startup script with pm2 startup, which auto-detects your init system and, on modern distros, creates a systemd unit that owns the PM2 daemon. Then pm2 save freezes your current process list so PM2 can respawn everything after boot.[4] It is a two-step ritual, and it is worth internalizing it as routine, otherwise a reboot silently kills what you thought was managed.

systemd gets boot persistence by design. systemctl enable creates the symlinks that start the unit at boot, and WantedBy=multi-user.target in the [Install] section is the standard hook. There is no extra tool and no “save” step - the unit file is the declaration of boot persistence.[3] Because systemd is the first process the kernel starts (PID 1), it is inherently last-man-standing; your app cannot come up before the OS itself.

Crash handling and restarts

If the process dies, both will restart it - but their defaults and tuning knobs differ.

PM2 restarts by default and makes the behavior visible in pm2 list. You get options like --max-memory-restart <200MB> to recycle a process that exceeds a memory threshold and --restart-delay to space out automatic restarts.[1] For a single-process Node app this is often all the resilience you need, and pm2 monit gives you a live view out of the box.

systemd is more conservative by default: Restart= defaults to no, meaning systemd will not restart a service unless you opt in. Set Restart=on-failure (restart on unclean exit) or Restart=always. Restart timing is controlled by RestartSec=, which defaults to 100ms - almost always too aggressive, so you typically raise it to a couple of seconds to give the app a moment to, say, release the port and clean up sockets.[5]

Two systemd details are worth flagging because most people miss them. RestartSec= does not only apply to Restart= - it also throttles RestartMaxDelaySec/RestartSteps exponential backoff, meaning unusually frequent crashes get progressively delayed rather than hammered in a tight loop.[5] And for Type=oneshot services, Restart=always and Restart=on-success are not allowed at all, which you will not run into if your Node app is a long-running server but should know before you cargo-cult a snippet.[5]

CPU scaling and clustering

This is the one area where PM2 genuinely shines for Node apps, and it is the reason many teams adopt it at all.

PM2 cluster mode uses the Node.js cluster module under the hood to scale a networked app across all CPUs without touching your source. pm2 start app.js -i max auto-detects the CPU count and runs as many processes as available.[2] pm2 reload then achieves a zero-downtime reload - for networked apps - by cycling workers one at a time instead of killing everything at once, whereas restart stops and starts the whole pool.[2] If your deployment process demands zero-downtime rollouts, this out of the box is hard to beat.

systemd gives you nothing like this for free. It will happily supervise N copies of your process (N unit files, or a template unit like my-api@.service), but it has no concept of “cluster mode” and does nothing to share a port or balance connections between them. To get the same result you must implement the clustering yourself with the Node.js cluster module.

The Node.js cluster docs are worth reading before you assume more workers is always better. The cluster module supports two connection-distribution methods: round-robin, where the primary process listens and hands connections to workers in rotation (the default on all platforms except Windows), and a shared-socket approach. The docs are candid that the shared-socket approach’s distribution can be badly unbalanced in practice - load observations showed over 70% of connections landing on just two out of eight processes.[6] Cluster mode is powerful, but it is not “spin up as many as you can and it’s strictly faster,” and your app must be stateless (sessions, sockets, in-memory caches shared elsewhere) to benefit. PM2’s own docs say exactly that: a clustered app is only as good as its statelessness.[2]

Resource limits

If you want to cap how much memory or CPU a process can consume, systemd is the stronger tool because it sits directly on the kernel’s control-group (cgroup) machinery.

systemd uses Linux Control Groups (cgroups) internally for resource management, and its resource-control directives (memory.max style limits exposed through MemoryMax=, CPUWeight=, TasksMax=, and many more) are configured in the [Service] section of your unit file. Controllers are enabled automatically the moment you set a matching option - for example, setting CPUWeight= enables the cpu controller and setting a task limit enables the pids controller.[7] You can pin a Node app to 512 MB of RAM, cap its CPU share, or bound its file descriptors with settings that apply at the kernel level, so the limit genuinely holds.

PM2 cannot enforce kernel-level cgroup limits. Its closest tool is the --max-memory-restart threshold, which reacts - it kills and restarts the process after the fact rather than preventing it from exceeding a limit. For a hard guarantee, systemd wins cleanly.

Logging and observability

Both give you logs; they just differ in where they live and what you get alongside them.

PM2 owns log management: pm2 logs streams output, --lines 200 digs into history, and it can write to per-app files and prefix entries with timestamps. It pairs with the paid pm2.io dashboard for cross-server metrics.[1] For a team used to pm2 semantics, this is frictionless.

systemd sends everything to the journal, which you read with journalctl -u my-api.service. Because the journal covers the whole system, a Node crash is correlated with kernel and other service events in one place - something PM2’s isolated logs cannot give you. Interleaving your app’s stderr with OOM-killer and network events is a genuinely useful debugging superpower for diagnosing crashes, and it works with zero configuration.

A note on security hardening

Since systemd runs your process as a system service, you get its hardening profile for free. The execution options live in systemd.exec, and sensible production units typically run the app as an unprivileged user (User=www), drop PrivateTmp, and disable privileged helpers with NoNewPrivileges=. These are the same restrictions applied to other system services, and they cost nothing to declare in the unit file.[8] PM2 runs the process with the privileges of the user who started the daemon, so the burden of least-privilege is on you.

The honest recommendation

There is no universal winner; the two tools solve different parts of the problem, and the best setups usually use both.

Pick PM2 (possibly over a systemd-run PM2 daemon) when you want clustering and zero-downtime reload with zero code changes, when you want Node-flavored tooling (pm2 list, pm2 logs, pm2 monit), or when your team is already fluent in it. It is the fastest path to a multi-CPU production setup for a stateless Node API.

Pick systemd when the serving is single-process or natively clustered in your app, when you want kernel-level resource limits and system-wide journal correlation, when you want the app governed by the same hardened machinery as the rest of the OS, or when “no extra moving part to keep alive” is a priority - systemd needs no daemon pm2 start telemetry, no node_modules dependency, and no pm2 save memory.

The pragmatic note: PM2’s own persistence layer is a systemd unit - pm2 startup on a modern distro generates a pm2-<user>.service that owns the daemon.[4] So a lot of “PM2 vs systemd” is really “a systemd unit that runs your Node app” versus “a systemd unit that runs PM2, which runs your Node app.” If you have already accepted systemd at the boot layer, adding PM2 on top is an extra layer you pay for in complexity and only get clustering and its CLI in return.

Finally, the production version of any claim “Node.js handles it well” only holds on a supported runtime. Node.js documentation is explicit that production applications should use only Active LTS or Maintenance LTS releases, and LTS status guarantees critical-bug fixes for a total of 30 months.[9] Whatever manager you choose, pin your app to an LTS line - as of this writing that is Node.js 26 (Active LTS), with 24 (Krypton) and 22 (Jod) on Maintenance LTS.[9]

This comparison assumes a bare process manager. If you’re weighing a container-based setup instead, our Docker vs VPS guide covers that decision, and the end-to-end deploy shows the manual path this manager slots into. If you’d rather not run a process manager at all, a self-hosted PaaS handles it for you - the coolify vs dokploy comparison covers the main options.

Sources

  1. PM2 - Quick Start (installation, CLI options, logs, monit). https://pm2.keymetrics.io/docs/usage/quick-start/ - checked 2026-09-28
  2. PM2 - Cluster Mode (cluster scaling, -i max, zero-downtime reload, stateless requirement). https://pm2.keymetrics.io/docs/usage/cluster-mode/ - checked 2026-09-28
  3. systemd.service - Type= semantics, Type=exec recommendation, unit file structure. https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html - checked 2026-09-28
  4. PM2 - Startup Script (pm2 startup, auto-detection of systemd, pm2 save). https://pm2.keymetrics.io/docs/usage/startup/ - checked 2026-09-28
  5. systemd.service - Restart= (default no), RestartSec= (default 100ms), backoff/throttling. https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html - checked 2026-09-28
  6. Node.js - Cluster module (round-robin default, shared-socket imbalance observation). https://nodejs.org/api/cluster.html - checked 2026-09-28
  7. systemd.resource-control - cgroups-based resource control, controller auto-enable. https://www.freedesktop.org/software/systemd/man/latest/systemd.resource-control.html - checked 2026-09-28
  8. systemd.exec - execution environment and hardening options. https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html - checked 2026-09-28
  9. Node.js - Releases (Active LTS / Maintenance LTS requirement, 30-month LTS guarantee; v26 Lithium = Active LTS, v24 Krypton / v22 Jod = Maintenance LTS). https://nodejs.org/en/about/previous-releases - checked 2026-11-06