PM2 vs systemd for Node.js Services

Khimananda Oli 9 min read Programming and Languages
PM2 vs systemd for Node.js Services

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.

PM2 ArchitecturePM2 Daemon (Node.js)App Instance 1App Instance 2App Instance NShared Log Files (~/.pm2/logs)Single Point of Failure Risksystemd Architecturesystemd (PID 1 / Kernel)Service Unit A(Isolated cgroup)Service Unit B(Isolated cgroup)Service Unit C(Isolated cgroup)journald (Binary Logs + Metadata)Independent Service Recovery
PM2 runs as a user-space Node.js daemon managing child processes, while systemd manages each Node.js service as an independent kernel-supervised unit with isolated resources.

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() and exec() 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.

systemdNode.js AppjournaldKernel/cgroupsExecStartRegister cgroup limitsstdout/stderr → journalSIGSEGV / Exit(1)Check RestartSec & policyRe-exec binaryLog restart eventEnforce MemoryMax/CPUQuota
systemd lifecycle: direct exec, kernel-enforced resource limits, structured journal logging, and automatic restart without intermediary daemons.

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.

  1. Development and staging environments: PM2's --watch flag 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.
  2. 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.
  3. 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 the cluster module or create multiple templated units ([email protected]). For monolithic apps not designed for horizontal scaling, PM2's zero-config clustering is valuable.
  4. 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.

CriterionPM2systemd
Log StoragePlain text files (~/.pm2/logs)Binary journal (/var/log/journal)
RotationExternal module (pm2-logrotate)Built-in vacuum policies
MetadataTimestamp only (manual parsing)PID, UID, unit, priority (automatic)
Resource IsolationNone (shared daemon memory)cgroups v2 (MemoryMax, CPUQuota)
Security BoundariesUser-level onlyNamespace, capability, filesystem restrictions
ClusteringBuilt-in (-i max)Manual (cluster module or templated units)
Cross-PlatformLinux, macOS, WindowsLinux only
Startup OverheadNode.js daemon bootstrap (~200-500ms)Native exec (<10ms)
Audit ComplianceDifficult (no tamper-proof logs)Native (journal integrity, SELinux/AppArmor)
PM2 vs systemd Trade-off MatrixDeveloper VelocityProduction ReliabilityHot Reload & Watch ModeKernel-Level SupervisionZero-Config Clusteringcgroup Resource IsolationCross-Platform SupportAudit-Ready LoggingBundled Monitoring UISecurity Hardening (Namespaces)Choose based on lifecycle stage: PM2 for dev/staging velocity, systemd for prod/compliance
Trade-off visualization: PM2 optimizes for developer experience and portability, while systemd prioritizes isolation, security, and operational compliance.

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.

Frequently Asked Questions

Use systemd for production stability and OS integration. Reserve PM2 for development, debugging, or when you need built-in cluster mode without writing custom wrapper scripts.

Yes. PM2 runs a separate daemon process consuming roughly 30-50MB RAM overhead, while systemd manages services directly through the kernel with negligible additional memory footprint.

No. Systemd lacks native clustering. You must implement node:cluster in your application code or use PM2 if you require zero-code multi-process scaling across CPU cores.

Set Restart=always and RestartSec=5 in your unit file. Systemd automatically respawns failed processes using exponential backoff, providing superior crash recovery compared to basic PM2 restart policies.

PM2 is generally safe but runs as a user-space daemon. Systemd offers stronger isolation via DynamicUser, ProtectSystem, and capability restrictions, making it the preferred choice for security-hardened production deployments.

PM2 buffers logs internally requiring manual rotation setup. Systemd integrates with journald for automatic structured logging, binary storage, rate limiting, and seamless forwarding to external aggregation systems like Vector.

Avoid running both simultaneously for the same service. Conflicting process supervisors cause port binding failures, duplicate instances, and unpredictable restart loops during deployment or system updates.

PM2 requires full user write access to home directories and /tmp. Systemd services can run as restricted dynamic users with read-only filesystems and no persistent home directory requirements.

Use EnvironmentFile=/etc/myapp/env pointing to a root-owned 0600 file. This prevents secrets from appearing in process listings, unlike PM2 ecosystem files which may expose values in pm2 jlist output.

Yes. Configure TimeoutStopSec=30 and send SIGTERM via KillMode=mixed. Your Node app should handle the signal to close database connections and finish pending requests before forced termination.

Neither handles rollbacks natively. Pair systemd with CI/CD tools like Ansible or GitHub Actions for atomic deploys. PM2 rollback commands only revert local file changes without infrastructure state awareness.

Yes. Create template units like [email protected] and instantiate via systemctl enable [email protected]. This scales to hundreds of services with shared configuration and individual lifecycle control.

PM2 requires explicit pm2 startup and save commands to persist. Missing this step or incorrect user context causes services to vanish after reboot, whereas systemd units enable persistently by default.

Yes. PM2 starts serving with one command. Systemd requires writing unit files, setting permissions, and reloading the daemon, adding fifteen minutes of initial configuration time.

Define ExecStartPost health checks or use Type=notify with sd_notify in your app. Combine with systemd watchdog timers for automatic restarts on unresponsive processes without external monitoring agents.