What I learned building a lightweight production supervisor that can manage Bun, Node.js, Python, Go, Rust, Ruby, PHP, Java, and native applications.
Disclosure: I am the creator of ProcBoss, the open-source project discussed in this article. This article focuses on the engineering problems and design decisions behind building it rather than presenting it as an independent review.
Getting an application running locally is usually the easy part.
The harder question comes when that application needs to stay running for days, weeks, or months.
What happens when the process crashes at 3 AM?
What happens when memory usage keeps growing?
How do you restart an application without taking the service completely offline?
How do you manage several workers?
How do you make sure applications come back after a server reboot?
And what happens when your server isn't running only JavaScript?
These are the problems process supervisors have been solving for years.
Tools such as PM2 have become popular because they provide a convenient layer between an application and the operating system: start it, monitor it, restart it when necessary, collect its logs, and keep it alive.
PM2 is still a mature production process manager and currently supports Node.js and Bun applications as well as Python, Ruby, binaries, clustering, startup scripts, and other production features.
But I wanted to explore a different question:
Could a modern process manager be built around Bun's native APIs while remaining useful for applications that don't use Bun at all?
That question eventually became ProcBoss.
The original idea was much smaller.
I was interested in building a process manager that felt natural in the Bun ecosystem instead of simply running an existing Node-oriented tool through Bun.
Bun exposes several APIs that are useful for this kind of systems software.
The most important one is Bun.spawn.
A simplified process launch looks like this:
const child = Bun.spawn({
cmd: ["bun", "run", "./server.ts"],
stdout: "pipe",
stderr: "pipe",
});
At first glance, that's enough to start a process.
A production process manager, however, isn't really about starting processes.
Starting a process is the easy part.
The difficult part is owning its entire lifecycle.
A supervisor needs to know:
The process itself is only one piece of the system.
The architecture gradually evolved around a daemon that owns the process lifecycle.
Conceptually, it looks like this:
┌─────────────────────┐
│ pboss CLI │
└──────────┬──────────┘
│
▼
┌─────────────────────┐
│ Daemon │
│ │
│ Process Supervisor │
│ State Management │
│ Event System │
│ Scheduler │
│ Health Checks │
└──────────┬──────────┘
│
┌─────────────────┼─────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Worker │ │ Worker │ │ Worker │
│ Bun/Node │ │ Python │ │ Go/Rust │
└──────────┘ └──────────┘ └──────────┘
The CLI doesn't need to own the processes directly.
It communicates with the daemon, while the daemon remains responsible for supervision.
That separation becomes important once you introduce dashboards, APIs, remote management, scheduled jobs, and multiple clients.
One design decision changed the direction of the project.
A process manager built with Bun doesn't necessarily need to manage only Bun applications.
The operating system doesn't care whether the executable was written in TypeScript, Go, Rust, Python, Java, Ruby, PHP, or C.
If the supervisor's responsibility is process lifecycle management, then limiting it to one runtime creates an artificial boundary.
So ProcBoss evolved into a universal process manager.
Today it can manage applications including:
Runtime detection happens automatically in the normal workflow.
That means a server can look something like:
ProcBoss
│
┌───────────────┼────────────────┐
│ │ │
API.ts worker.py service
│ │ │
Bun Python Go
The supervisor is Bun-native.
The applications don't have to be.
The basic restart loop is straightforward:
process exits
│
▼
determine exit reason
│
├── intentional ──► remain stopped
│
└── unexpected
│
▼
restart policy
│
▼
start process
But real applications introduce more cases.
A process might exit because the user explicitly stopped it.
It might crash.
It might exceed a memory limit.
A health check might fail repeatedly.
A file watcher might trigger a restart.
A scheduled restart might occur.
These aren't equivalent events.
ProcBoss therefore treats lifecycle changes as events with causes such as user, crash, memory, watch, cron, and health.
That distinction becomes useful beyond logging.
A monitoring system can answer not just:
"The process restarted."
but:
"The process restarted because its health check failed."
That is a much more useful piece of information.
Running one process is rarely enough for a modern application.
A typical application might contain:
my-app
├── web
├── worker
├── scheduler
└── websocket
You don't necessarily want to issue four independent commands every time you deploy or restart the application.
This led to namespaces.
A namespace groups processes into a single lifecycle unit:
pboss start web.ts --name web --namespace my-app
pboss start worker.ts --name worker --namespace my-app
Now the group can be operated as one unit:
pboss restart my-app
But grouping processes introduces another difficult problem.
Imagine this:
web ✓ started
worker ✓ started
scheduler ✗ failed
A naive implementation leaves the first two processes running even though the namespace never successfully started.
That creates a partially initialized application.
Instead, ProcBoss uses atomic namespace startup behavior: when a namespace startup fails, it rolls back only the processes that that particular invocation started. Processes that were already running remain untouched.
This is a small feature on the surface, but it represents an important principle:
Lifecycle operations should have predictable boundaries.
Once applications can be grouped, another problem appears.
Suppose the API requires Redis and PostgreSQL.
Starting the API before those services are available may result in an immediate crash.
So instead of simply saying:
start API
we can describe:
API
├── Redis
└── PostgreSQL
ProcBoss supports dependency declarations such as:
{
name: "api",
script: "./api.ts",
dependsOn: ["postgres", "redis"]
}
The supervisor can then resolve the dependency graph and start dependencies before dependents.
For example:
PostgreSQL ──┐
├──► API
Redis ───────┘
Independent branches can be started concurrently, while dependencies must be satisfied before their dependents proceed. ProcBoss also detects dependency cycles before attempting startup.
There's another interesting edge case here.
Not every dependency has to be another ProcBoss application.
A dependency can refer to a system service such as PostgreSQL managed by systemd.
ProcBoss can inspect whether that service is active without taking ownership of it.
That distinction is important.
A process manager should not accidentally decide that because your application depends on PostgreSQL, it now owns PostgreSQL's lifecycle.
Production applications often need multiple workers.
ProcBoss supports running multiple instances of the same application:
pboss start app.ts --name api --instances 4 --port 3000
The resulting processes can receive separate identities and automatically assigned ports.
Conceptually:
api-0 → :3000
api-1 → :3001
api-2 → :3002
api-3 → :3003
This provides a simple way to scale a process horizontally on a single machine.
It also creates another interesting problem: deployments.
Stopping all four processes simultaneously creates downtime.
Instead, workers can be reloaded progressively.
Worker 0 ── reload
Worker 1 ── reload
Worker 2 ── reload
Worker 3 ── reload
The exact strategy depends on how the application handles connections, but the important architectural idea is that a process manager can coordinate lifecycle transitions rather than simply killing and restarting everything.
Once a daemon owns your processes, it becomes a natural source of operational information.
ProcBoss exposes:
The project also includes a web dashboard with live WebSocket updates.
The dashboard isn't intended to replace every observability platform.
It's there to answer the questions you immediately have when something breaks:
Is it running?
What PID owns it?
How much memory is it using?
How many times has it restarted?
What did it log?
Is its health check passing?
Those questions should be answerable without SSHing into the machine and manually inspecting half a dozen commands.
Polling is easy.
It is also wasteful.
A dashboard can repeatedly ask:
Is the process still running?
Is the process still running?
Is the process still running?
Instead, the supervisor already knows when the process changes state.
So the daemon emits typed lifecycle events.
For example:
process:start
process:stop
process:restart
process:crashed
process:errored
process:reload
process:delete
Each event can also carry its cause.
This allows the CLI, dashboard, API clients, and modules to react to the same underlying event rather than implementing their own polling systems.
This became particularly useful as ProcBoss grew beyond a CLI.
A process manager eventually becomes an event source.
Capturing stdout and stderr sounds trivial until applications run continuously.
A production supervisor needs to think about:
ProcBoss captures application logs and supports size-based rotation, retention, and gzip compression.
The goal isn't to become a full log aggregation platform.
It's to make sure that a process dying at 4 AM doesn't leave you with either no logs or a 40 GB log file.
Another realization was that process management and scheduled execution overlap.
A machine might need to run:
database cleanup
backup script
report generator
cache warmer
certificate check
These aren't necessarily long-running processes.
So ProcBoss also supports standalone scheduled jobs.
For example, schedules can express human-readable timing such as:
everyday@9:11
or a specific date/time.
The important part is that the scheduler doesn't require the job to become a permanently managed process.
This turns the process supervisor into a small operational layer for the server rather than merely a restart utility.
Containers changed another assumption about process managers.
Traditionally, a process manager runs as a daemon on a server.
Containers often need something different.
If ProcBoss runs as PID 1, it can become the foreground supervisor:
pboss start app.ts --no-daemon
This is useful for container and Kubernetes environments where the process manager itself needs to remain attached to the container lifecycle.
The architecture therefore has two modes:
Traditional server
CLI → daemon → application
Container
container
│
└── pboss (PID 1)
│
└── application
The same supervisor can therefore fit both traditional VPS deployments and container-oriented environments.
A process that survives a crash but disappears after a server reboot isn't particularly useful in production.
ProcBoss therefore persists the process configuration and installs a per-user boot service appropriate to the operating system.
The intended lifecycle becomes:
server reboot
│
▼
pboss boot service
│
▼
restore process state
│
▼
dependency-aware startup
│
▼
applications online
This removes another manual recovery step from the administrator.
One of the motivations behind using Bun was avoiding unnecessary layers.
The current ProcBoss implementation is built around Bun's native APIs rather than requiring a large Node.js dependency stack.
The repository reports a daemon startup time of under 50 ms and approximately 12 MB of RAM for its footprint. These are project-reported measurements rather than independent benchmarks, so they should be treated accordingly.
The interesting part isn't the number itself.
It's the design goal:
The infrastructure responsible for keeping your application alive shouldn't become one of the largest applications on the server.
On a large cloud instance, a few megabytes may not matter.
On a small VPS, home server, development machine, or edge device, it can.
Building ProcBoss has changed how I think about Bun.
Bun is usually discussed in terms of web applications, package installation, JavaScript execution, and TypeScript development.
But the same runtime also exposes primitives useful for systems-oriented tooling:
Bun.spawn
Bun.serve
Bun.file
WebSocket
Bun.gzipSync
Those APIs aren't specifically a process manager.
They are building blocks.
The interesting part is combining them into a system that has to reason about operating-system processes, lifecycle state, scheduling, networking, files, resources, and failures.
That is a very different problem from serving an HTTP request.
Ironically, the hardest part of building ProcBoss wasn't learning Bun APIs.
It was deciding what the process manager should actually be responsible for.
Every feature introduces another lifecycle question.
Add clustering:
What happens during reload?
Add namespaces:
What happens when one member fails?
Add dependencies:
What happens when the dependency is already running?
Add system services:
Who owns the service?
Add health checks:
How many failures constitute an unhealthy process?
Add automatic restarts:
What distinguishes a crash from an intentional stop?
Add persistence:
What happens after reboot?
Add events:
How does every client receive the same state transition?
The deeper the project went, the less it looked like a collection of CLI commands.
It became a state-management system for processes.
The project originally started from a much narrower idea: build a lightweight, Bun-native supervisor for Bun applications.
That early version exposed a simple API for starting and managing Bun processes.
Over time, the scope changed.
The project became universal.
It gained clustering, namespaces, dependency graphs, health checks, scheduling, persistence, events, metrics, a dashboard, and support for multiple operating systems and runtimes.
That evolution also changed the design philosophy.
The goal is no longer:
"Build a process manager for Bun."
It is:
"Use Bun to build a lightweight process manager that happens to be capable of managing almost anything the operating system can execute."
That distinction matters.
If I were starting again, I would spend more time designing the lifecycle model before implementing the CLI.
It's tempting to start with commands:
start
stop
restart
list
logs
But those commands are just interfaces over a deeper state machine.
The important questions are:
What states can a process have?
What transitions are legal?
Who initiated the transition?
What caused it?
What happens if the transition fails?
What other processes are affected?
What state must survive a daemon restart?
Once those questions are answered, the CLI becomes much easier to design.
Without that model, every new feature tends to add another special case.
Building a process manager taught me something that applies beyond ProcBoss.
Infrastructure software often looks deceptively simple from the outside.
A command like:
restart api
can hide an enormous amount of state.
The process might have dependencies.
It might be part of a cluster.
It might belong to a namespace.
It might be unhealthy rather than dead.
It might have exceeded a memory threshold.
Another service might depend on it.
A deployment might currently be replacing it.
A dashboard might be watching it.
A monitoring system might be waiting for an event.
The command is simple because the complexity has been moved underneath it.
That is what good infrastructure tooling is supposed to do.
ProcBoss is now an open-source universal process manager built around Bun's native APIs.
It can manage applications written in different languages, run clustered workloads, group processes into namespaces, resolve dependencies, perform health checks, expose metrics, manage logs, schedule jobs, persist processes across reboots, and provide a live dashboard.
But the project is still evolving.
There are plenty of things a process manager can eventually become:
For me, the interesting part is that the original question remains relevant:
How much infrastructure can we build directly on top of a modern runtime without dragging an older architecture along with it?
ProcBoss is one experiment in answering that question.
And the experiment started with something as simple as spawning a process.
A process manager isn't glamorous software.
Nobody celebrates when their application stays running for six months.
That's the point.
The best infrastructure becomes invisible.
You deploy your application, walk away, and expect it to still be there tomorrow.
Building ProcBoss has been an exploration of what happens when you take that boring requirement seriously and build the entire supervision layer around a modern runtime.
Bun provided the primitives.
The operating system provided the processes.
The difficult part was everything in between.
And that turned out to be the interesting part.
Project: ProcBoss - an open-source universal process manager built with Bun.
Repository: https://github.com/Procboss/pboss