All-seeing runtime diagnostics for Node.js.
One line in your process. Event loop lag, memory, GC, stream backpressure and request traces.
Zero dependencies in the agent, no hosted account, no separate backend.
npm install @pharn-dev/phargus
node --import @pharn-dev/phargus/agent app.mjs | npx phargus serveStatus: 0.2.0 is the latest release on npm (
lateston the npm registry; 0.1.0, the first release, was published 2026-10-12).maincan be ahead of it: changes after 0.2.0 are listed underUnreleasedinCHANGELOG.md. Roadmap steps S0 to S7 are implemented (agent, collector, dashboard, tracing, V8 and memory depth, plugin runner, OpenTelemetry export; seeROADMAP.md).
- Why Phargus
- Quick start
- Agent skills
- Configuration
- What you get
- How it fits together
- Safety and limits
- Documentation
- Contributing
- Security
- License
Phargus is for a quick look at a live process: one line, in-process, no account.
| One-line setup | Runs in the live process | Needs an account | Analysis | |
|---|---|---|---|---|
| clinic.js | no | one-shot capture | no | offline, after |
| OpenTelemetry | no | yes | no, but a backend | in your backend |
| Hosted APMs | yes | yes | yes, and paid | in the vendor's UI |
| Phargus | yes | yes | no | live, in your browser |
The agent's zero-dependency rule is never relaxed: src/agent imports Node core only, and an ESLint
rule enforces it.
Requires Node.js 22.18 or newer (engines.node is >=22.18.0; .nvmrc pins the
tested floor). On Node 22, request tracing is noticeably more expensive for promise-heavy code than
on Node 24; see docs/LIMITS.md.
Two commands: install the package, then run your app with the agent and pipe its output into
phargus serve.
npm install @pharn-dev/phargus
node --import @pharn-dev/phargus/agent app.mjs | npx phargus servephargus serve prints phargus: dashboard at http://127.0.0.1:7070 on stderr; open that address.
Your app's stdout is the pipe, so it may log there too: lines that are not agent data are skipped
and counted. When the app exits, the dashboard keeps serving what it collected until you press
Ctrl-C.
For a CommonJS app, use --require instead of --import:
node --require @pharn-dev/phargus/agent app.js | npx phargus serveOr load the agent in code, first, before your own code:
// CommonJS
require('@pharn-dev/phargus/agent');
// ESM
import '@pharn-dev/phargus/agent';By default the agent writes one sample line per second (about 2 KB) to your process's stdout,
plus one line per traced request. That is all the agent does on its own: it starts no server.
phargus serve (below), or a collector you wire yourself, turns that stream into the dashboard.
phargus serve [options] [file|-]
| Option | Default | Meaning |
|---|---|---|
--port <n> |
7070 |
Port to listen on, 0 to 65535. 0 picks a free port. |
--host <address> |
127.0.0.1 |
Address to bind. A non-loopback host needs a token. |
--token <value> |
none | Require a token. Prefer PHARGUS_TOKEN: a flag shows in the process list. |
--allowed-host <host> |
none | Also answer this Host name. Repeatable; host:port matches exactly. |
--window-ms <n> |
1000 |
Aggregation window in milliseconds. |
--max-line-bytes <n> |
1048576 |
Longest accepted input line in bytes. |
-h, --help |
Print the usage text. | |
-v, --version |
Print the version. |
-
Input. stdin by default (or
-). Give a file path to replay a capture: redirect the app's stdout to a file, then runnpx phargus serve capture.ndjson. The server keeps running after the input ends, serving what it collected, until you stop it. -
Token. On loopback no token is needed. For any other
--hostset one, withPHARGUS_TOKEN=... npx phargus serve --host 0.0.0.0; without it the command exits with code 2 before binding.PHARGUS_TOKENandPHARGUS_DEBUGbelong tophargus serve; the agent ignores both, so they can sit in an environment shared with the app. See Dashboard access under Using the library APIs for how browsers and scripts present the token. -
Reaching it from another machine. The server answers only the
Hostnames listed under Dashboard access; any other gets421. When the browser uses a different IP or hostname, for example with--host 0.0.0.0in a container, name it with--allowed-hostand repeat the flag for several names:PHARGUS_TOKEN=... npx phargus serve --host 0.0.0.0 \ --allowed-host dash.internal --allowed-host 192.0.2.10
A bare name matches on any port, so it also works behind a port mapping;
host:portmatches only that port. The value is a host, not a URL: an empty value, whitespace or/is a usage error (exit2).--allowed-hostdoes not replace the token. The dashboard is plain HTTP, so across an untrusted network still use an SSH tunnel or a TLS-terminating proxy. -
Diagnostics go to stderr only. The dashboard URL, a line when the input ends, and a
counts:line (decoder.invalid,decoder.oversized,collector.ignored,dashboard.droppedEvents) when the input ends and again at exit. Errors print without a stack; setPHARGUS_DEBUG=1to get one. -
Exit codes.
0for help, version and a graceful stop onSIGINTorSIGTERM;1for a runtime error (unreadable file, port in use) or a shutdown that did not finish in 5 seconds;2for a usage error (unknown flag, bad value, non-loopback host without a token).
To embed the collector and dashboard in your own process, wire @pharn-dev/phargus/collector and
@pharn-dev/phargus/dashboard yourself. A minimal version, which is what phargus serve does:
// collect.mjs — run as: node --import @pharn-dev/phargus/agent app.mjs | node collect.mjs
import { pipeline } from 'node:stream/promises';
import { createCollector, createNdjsonDecoder } from '@pharn-dev/phargus/collector';
import { createDashboardServer } from '@pharn-dev/phargus/dashboard';
const collector = createCollector({ windowMs: 1000, capacity: 300 });
const dashboard = await createDashboardServer({ collector, host: '127.0.0.1', port: 7070 });
process.stderr.write(`phargus dashboard on ${dashboard.url}\n`);
const decoder = createNdjsonDecoder();
await pipeline(process.stdin, decoder, (records) => collector.consume(records));Your app may write to the same stdout. Lines that are not JSON objects (plain text, truncated
lines) and lines longer than maxLineBytes (1 MiB by default) are skipped and counted as
decoder.invalid and decoder.oversized; JSON objects that are not agent records, such as the
lines of a JSON logger, and agent-shaped records that cannot be aggregated are skipped and counted
as collector.ignored. Neither is thrown. Pass onSkipped to createNdjsonDecoder or onIgnored
to createCollector to see each skipped line or record.
examples/express-app is the complete version: it forks the app, reads
its stdout into a collector with an alert rule, serves the dashboard, and shuts both down cleanly.
The recording above is that example.
Dashboard access.
- On a loopback address (
127.x.x.x,localhost,::1) the token is optional. Binding any other address without atokenis refused at startup. - With a token, open
http://<host>:<port>/login#token=<token>once in a browser. The token is in the URL fragment, which the browser never sends to the server. The login page removes the fragment from the address bar and history, then posts the token toPOST /sessionin a JSON body; the server answers204with anHttpOnly,SameSite=Strictcookie and the page opens/. A wrong token gets401and no cookie. http://<host>:<port>/?token=<token>still works for one release and is deprecated: it puts the token in a request line. The server emits aPHARGUS_DEP_TOKEN_QUERYdeprecation warning the first time it is used.?token=is accepted on/only.- Scripts and
curlsendAuthorization: Bearer <token>, for example to/events. - The server answers only requests whose
Hostis the bound address on the bound port (pluslocalhost,127.0.0.1and[::1]for a loopback or0.0.0.0/::bind); anything else gets421. A cross-originOriginon/eventsgets403. Behind a reverse proxy, or when you reach the dashboard by another name, list that name inallowedHosts(--allowed-hostforphargus serve). - The dashboard is plain HTTP. For remote access use an SSH tunnel or a TLS-terminating proxy.
If you work with a coding agent, pharn-dev/skills ships a
phargus plugin that sets up and diagnoses Phargus for you. Ask in plain language; the agent picks
the skill.
| Skill | What it does |
|---|---|
phargus-setup |
Connects Phargus to your app and verifies the dashboard receives real samples |
phargus-diagnose |
Investigates slow requests, event-loop stalls, GC pressure and stream backpressure |
phargus-verify |
Compares before and after measurements and says whether a fix helped, regressed or is inconclusive |
phargus-memory |
Separates heap, RSS and buffer growth and reads heap snapshot summaries and diffs |
phargus-rules |
Turns an observed condition into a collector alert or sandboxed rule, replayed before you enable it |
Install for Claude Code:
/plugin marketplace add pharn-dev/skills
/plugin install phargus@pharn-dev
Or for any supported agent with the Skills CLI:
npx skills add pharn-dev/skillsThe skills are a development draft targeting the 0.1.x runtime; see the skills README for Codex and compatibility details.
Choose where the agent writes with PHARGUS_OUTPUT:
| Variable | Default | Meaning |
|---|---|---|
PHARGUS_OUTPUT |
stdout |
stdout, none (agent off), or a file path |
PHARGUS_INTERVAL_MS |
1000 |
sampling interval |
PHARGUS_QUEUE_BOUND |
1024 |
lines held per output queue before the oldest are dropped |
PHARGUS_ENABLED |
true |
false or 0 turns the agent off |
PHARGUS_CONFIG |
none | path to the one config file to load, .json or .js; skips the working-directory lookup |
The same keys (output, intervalMs, queueBound, enabled) can go in phargus.config.json or
phargus.config.js in the working directory. An unknown PHARGUS_* variable prints one warning on
stderr and is ignored; PHARGUS_TOKEN and PHARGUS_DEBUG, which belong to phargus serve, are
ignored without a warning. When PHARGUS_CONFIG is set, phargus.config.json and
phargus.config.js in the working directory are not read, and a problem with the pinned file
disables the agent like any invalid value. An invalid value disables the agent with one
[phargus] agent disabled: ... line on stderr; your app keeps running.
| Capability | What it covers | Import |
|---|---|---|
| Runtime samples | Event loop delay beyond the 10 ms timer interval (min, max, mean, p50, p99; an idle loop reads close to 0); memory (heapUsed, heapTotal, rss, external, arrayBuffers); V8 heap space statistics; GC count, total and max pause and counts by kind |
@pharn-dev/phargus/agent |
| Backpressure | How often and how long writes stalled, grouped by the call site that wrote. Covers Writable subclasses, fs.WriteStream, Duplex, Transform, PassThrough, net.Socket, tls.TLSSocket, http.ServerResponse and http.ClientRequest |
@pharn-dev/phargus/agent |
| Request tracing | Each request to a node:http or node:https server gets a trace id (or continues an incoming W3C traceparent), carried by AsyncLocalStorage with no change to your code. Spans record method, path without the query string, status and duration. currentTraceId() reads the id anywhere in that request's async work |
@pharn-dev/phargus/agent |
| Loss accounting | Output queues are bounded and drop the oldest lines; every sample line carries cumulative dropped: { samples, spans } counters |
@pharn-dev/phargus/agent |
| V8 and memory | On-demand heap snapshots (takeHeapSnapshot); snapshot summaries, diffs and stack-trace symbolization in a Worker Thread pool; allocation sampling (sampleAllocations); a parser for node --trace-deopt output (createDeoptParser, you feed it the lines) |
@pharn-dev/phargus/analyzer |
| Collector | Fixed time windows with integer math, threshold alerts, alert sinks (stdout, file, webhook), opt-in persistence of recent windows to an append-only file | @pharn-dev/phargus/collector |
| Dashboard | Vanilla JS, no framework, no bundler, live over Server-Sent Events: latest event loop, memory and GC figures, alerts, and a waterfall of recent requests | @pharn-dev/phargus/dashboard |
| Custom rules | User rules run in an isolated-vm sandbox in a child process, with a memory limit, a timeout and capped results. isolated-vm is an optional peer dependency |
@pharn-dev/phargus/plugin-runner |
| OpenTelemetry | Opt-in OTLP/HTTP JSON export of windows as metrics and spans as traces, batched into bounded requests with dropped items counted, using Node core fetch only |
@pharn-dev/phargus/otel |
FEATURES.md lists the full scope with the status of each item.
createOtlpMetricsExporter (or toOtlpMetrics) turns each collector window into one data point
per metric, as integer asInt values. Gauges are stamped at the window end. Sums are delta and
monotonic, and their interval is the window.
| Metric | Type | Unit | Window field |
|---|---|---|---|
phargus.event_loop.lag.max |
gauge | ns |
eventLoop.max |
phargus.event_loop.lag.p99 |
gauge | ns |
eventLoop.p99 |
phargus.event_loop.lag.mean |
gauge | ns |
eventLoop.mean |
phargus.memory.heap_used.last |
gauge | By |
memory.heapUsedLast |
phargus.memory.heap_used.max |
gauge | By |
memory.heapUsedMax |
phargus.memory.rss.last |
gauge | By |
memory.rssLast |
phargus.memory.rss.max |
gauge | By |
memory.rssMax |
phargus.gc.pause.max |
gauge | ns |
gc.maxPause |
phargus.backpressure.stall.max |
gauge | ns |
backpressure.maxStall |
phargus.gc.count |
sum | {collection} |
gc.count |
phargus.gc.pause.total |
sum | ns |
gc.totalPause |
phargus.backpressure.events |
sum | {event} |
backpressure.events |
phargus.backpressure.stall.total |
sum | ns |
backpressure.totalStall |
phargus.window.samples |
sum | {sample} |
count |
phargus.window.late_samples |
sum | {sample} |
late |
The resource carries service.name (option serviceName, default phargus),
service.instance.id (option serviceInstanceId, default a random UUID generated once per
process), host.name and process.pid, so several instances of one service stay separate series.
The trace exporter's resource still carries service.name only.
flowchart LR
subgraph app["your process"]
code["app code"] --- agent["agent<br/>samplers, HTTP tracing,<br/>bounded output"]
end
subgraph second["a second process (or the same one)"]
collector["collector<br/>windows, alerts, sinks"] --> dashboard["dashboard<br/>SSE to the browser"]
collector -.-> otel["otel<br/>(opt-in)"]
collector -.-> rules["plugin-runner<br/>(opt-in)"]
end
agent -- "NDJSON" --> collector
The agent never waits for the consumer: if the reader is slow, the agent drops the oldest queued lines and counts them, so a slow consumer cannot slow your app.
Phargus is one npm package: the six modules are subpath exports, shipped as both ESM and CommonJS.
analyzer runs its CPU-heavy work in Worker Threads. See ARCHITECTURE.md for
the data flow and module boundaries.
| Claim | The check behind it |
|---|---|
| The agent has no third-party dependencies | An ESLint rule limits src/agent to node: builtins and its own files, and bars imports from the other modules |
| Every subpath loads as ESM and as CommonJS | npm run check:exports loads each exports entry via import and require |
| The published tarball is what we intend | npm run check:package checks tarball contents, source maps and engines.node against .nvmrc |
| Overhead is measured, not assumed | bench/overhead.mjs (agent on versus off, several HTTP workloads) and bench/soak.mjs (leaks under load), run nightly. Informational: no gate fails on a regression, so measure on your own workload. See bench/README.md |
| No secrets in the repository | gitleaks on every push, plus CodeQL |
- Prototype patches are reversible and never throw. The backpressure probe wraps
writeon the stream prototypes and tracing wrapsServer.prototype.emit; disabling them never removes a wrapper another library installed later. - The agent runs under the Node permission model. The flags each module needs are in
docs/PERMISSIONS.md. - Heap snapshots and traces can contain sensitive data. See
SECURITY.md,docs/THREAT-MODEL.mdanddocs/LIMITS.md.
| Document | What is in it |
|---|---|
FEATURES.md |
Full feature scope and status |
ARCHITECTURE.md |
Data flow and who may import what |
docs/API.md |
Every supported export, per subpath |
docs/PERMISSIONS.md |
Node permission-model flags per module |
docs/LIMITS.md |
Cost, overhead and known limits |
docs/THREAT-MODEL.md |
What Phargus defends against and what it does not |
ROADMAP.md |
Build order and status |
CHANGELOG.md |
Release notes |
Contributions are welcome. Start with CONTRIBUTING.md, which lists the rules
pull requests are checked against and how to build from source, then CLAUDE.md for
the hard constraints. Look for issues labeled good-first-issue. By participating you agree to
follow the Code of Conduct.
Please do not open public issues for vulnerabilities. Report them privately as described in
SECURITY.md.
