
Table of Contents
By Khimananda Oli | Last reviewed: August 2026
Choosing the right process manager is one of the first operational decisions you make when deploying a Node.js application to a Linux VPS or bare-metal server. The debate over PM2 vs systemd for Node.js services usually comes down to developer convenience versus native operating system integration. While PM2 offers an excellent developer experience and built-in clustering, systemd provides superior resource isolation, security hardening, and zero-dependency reliability for long-running production workloads.
If you are setting up a fresh server, understanding this distinction prevents architectural debt later. I have managed fleets of Node.js microservices across AWS EC2 and on-premise hardware in Nepal, and the pattern is consistent: teams that start with PM2 for its CLI convenience often migrate to systemd units once they hit compliance audits or memory leak issues. Before diving into the configuration details, it helps to understand how these tools fit into your broader server hardening and setup workflow.
How does PM2 vs systemd for Node.js services differ in core architecture?
The fundamental difference lies in where the process supervision happens. PM2 is itself a Node.js application. When you run pm2 start app.js, you are spawning a long-lived Node.js daemon that forks your application as child processes. This daemon maintains state in memory and on disk (~/.pm2/dump.pm2) to handle restarts and clustering. If the PM2 daemon crashes or runs out of memory, every application it manages can become orphaned or unmanageable until the daemon is manually resurrected.
systemd, by contrast, is the init system (PID 1) on virtually all modern Linux distributions. It does not run inside a runtime environment; it is compiled C code with direct kernel access. Each Node.js application gets its own .service unit file. systemd supervises the main process directly via signals and cgroups. There is no intermediate interpreter layer. If your Node.js app crashes, the kernel notifies systemd immediately, and the restart policy executes without any JavaScript overhead.
Process supervision models
- PM2: Uses
child_process.fork(). Restart logic depends on the health of the PM2 daemon. Clustering is handled internally by round-robin IPC. - systemd: Uses native
fork()andexec()syscalls. Supervision is tied to PID tracking and cgroup membership. No shared runtime state between services.
How do you configure a production-ready systemd unit for Node.js?
A common mistake engineers make when migrating from PM2 is writing minimal systemd units that lack security boundaries. In production, especially for client-facing applications or SOC 2 environments, you must restrict what the service can do. Below is a hardened unit file I use as a baseline for Node.js APIs.
[Unit]
Description=Node.js API Service
After=network.target postgresql.service
Wants=postgresql.service
[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/opt/nodeapp/current
ExecStart=/usr/bin/node dist/server.js
Restart=on-failure
RestartSec=5s
# Security Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/nodeapp/logs /opt/nodeapp/tmp
PrivateTmp=true
RestrictSUIDSGID=true
MemoryDenyWriteExecute=true
# Resource Limits
MemoryMax=512M
CPUQuota=80%
# Environment
EnvironmentFile=/etc/nodeapp/env
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nodeapp-api
[Install]
WantedBy=multi-user.target This configuration enforces least-privilege principles. ProtectSystem=strict makes the entire filesystem read-only except for explicitly listed paths. NoNewPrivileges=true prevents the process from gaining elevated permissions via setuid binaries. These settings are impossible to replicate with PM2 alone and are frequently required during security hardening audits.
Managing environment variables securely
Never hardcode secrets in unit files. Use EnvironmentFile= pointing to a root-owned file with 0600 permissions. For higher security requirements, integrate with HashiCorp Vault or AWS Secrets Manager and inject credentials at startup via an ExecStartPre= script. This aligns with the practices outlined in secrets management for CI/CD pipelines.
When should you choose PM2 over systemd for Node.js?
Despite systemd's advantages, PM2 remains the right tool for specific scenarios. Its value proposition is developer velocity and runtime flexibility, not raw infrastructure stability.
- Development and staging environments: PM2's
--watchflag and hot-reload capabilities significantly outpace systemd's restart cycle. When developers need instant feedback on code changes without rebuilding containers or reloading units, PM2 wins. - Non-Linux deployments: If you deploy Node.js on Windows Server or macOS (rare in production, but common in legacy enterprise), systemd is unavailable. PM2 provides cross-platform consistency.
- Built-in clustering without code changes: PM2's cluster mode (
-i max) automatically forks workers based on CPU cores. With systemd, you must either implement clustering in your Node.js code using theclustermodule or create multiple templated units ([email protected]). For monolithic apps not designed for horizontal scaling, PM2's zero-config clustering is valuable. - Ecosystem tooling: PM2 Plus, Keymetrics, and the built-in monit dashboard provide application-level metrics (event loop lag, heap usage) without additional instrumentation. If your team lacks observability infrastructure, this bundled monitoring bridges the gap temporarily.
However, treat PM2 as a transitional solution. Once you require audit trails, resource quotas, or integration with centralized logging like structured logging pipelines, the migration cost to systemd increases.
How do logging and observability compare between PM2 and systemd?
Logging is where the operational divergence becomes most apparent. PM2 writes to plain text files in ~/.pm2/logs/. You must configure log rotation separately (usually via pm2-logrotate module), parse unstructured output, and ship files to a central aggregator. This adds three moving parts that can fail independently.
systemd integrates directly with journald. Every log line carries metadata: timestamp, PID, UID, unit name, and priority level — automatically. There is no file rotation to configure; journald handles vacuum policies natively. You query logs with journalctl -u nodeapp-api --since "1 hour ago" and forward to Loki, Elasticsearch, or CloudWatch via journal exporters. For teams building monitoring stacks, journald's structured metadata simplifies correlation between application errors and system events.
| Criterion | PM2 | systemd |
|---|---|---|
| Log Storage | Plain text files (~/.pm2/logs) | Binary journal (/var/log/journal) |
| Rotation | External module (pm2-logrotate) | Built-in vacuum policies |
| Metadata | Timestamp only (manual parsing) | PID, UID, unit, priority (automatic) |
| Resource Isolation | None (shared daemon memory) | cgroups v2 (MemoryMax, CPUQuota) |
| Security Boundaries | User-level only | Namespace, capability, filesystem restrictions |
| Clustering | Built-in (-i max) | Manual (cluster module or templated units) |
| Cross-Platform | Linux, macOS, Windows | Linux only |
| Startup Overhead | Node.js daemon bootstrap (~200-500ms) | Native exec (<10ms) |
| Audit Compliance | Difficult (no tamper-proof logs) | Native (journal integrity, SELinux/AppArmor) |
What are the performance and resource overhead differences?
In benchmarks across multiple production systems, systemd consistently shows lower baseline overhead. PM2's daemon consumes 30–80MB of RSS just to exist, plus additional memory per managed process for IPC channels. On a 1GB VPS hosting a single API, that overhead matters. systemd's supervision footprint is effectively zero — it's already running as PID 1 regardless of whether you use it.
Restart latency also differs. When a Node.js process crashes under PM2, the daemon must detect the exit, consult its internal state, and fork a new child. This typically takes 200–500ms. systemd's restart is a direct execve() syscall after the configured RestartSec delay (often 100ms minimum). For services handling thousands of requests per second, that delta reduces error budgets during incident recovery.
Memory enforcement is where systemd truly separates itself. With MemoryMax=512M in the unit file, the kernel's OOM killer targets only that specific cgroup if the limit is breached. Under PM2, a runaway worker can consume all available memory before the daemon notices, potentially crashing sibling applications. For multi-tenant servers or cost-sensitive cloud instances, this isolation prevents cascade failures.
Making the final decision for your Node.js deployment
The choice between PM2 vs systemd for Node.js services is not about which tool is universally better — it is about matching the tool to your operational maturity and compliance requirements. If you are a solo developer shipping a side project or managing a staging environment where iteration speed trumps isolation, PM2's ergonomics justify the trade-offs. If you operate production systems subject to SLAs, security audits, or resource constraints, systemd's native integration provides guarantees that no user-space daemon can match.
My recommendation for teams in 2026: start with systemd unit files from day one, even in development. Use Docker or Podman locally to replicate the isolation model. Reserve PM2 for cases where its unique features (hot reload, cross-platform support) solve a concrete problem that systemd cannot. This approach avoids the painful migration path many teams experience when their PM2-managed fleet grows beyond five services.
If you need help designing hardened Node.js deployments, auditing existing process managers, or building compliant infrastructure for your team, reach out to discuss your architecture. I help organizations build systems that survive traffic spikes, pass audits, and let engineers sleep through the night.