bun-profiler
v0.4.3
Published
Continuous CPU profiling for Bun (JavaScriptCore) via Pyroscope/Grafana ingest — zero native dependencies
Maintainers
Readme
bun-profiler
Continuous CPU profiling for Bun via Pyroscope / Grafana — zero native dependencies.
Why this exists
Every other Node.js profiler package (@pyroscope/nodejs, @datadog/pprof, etc.) segfaults or silently fails on Bun because they call V8-specific native APIs that don't exist in JavaScriptCore (JSC). This package uses Bun's built-in node:inspector Profiler API directly, converts CDP profiles to Pyroscope's folded-stack format, and pushes them to your Pyroscope server.
Requirements
- Bun ≥ 1.3.7
- A running Pyroscope instance (self-hosted or Grafana Cloud)
Install
bun add bun-profilerUsage
import { startProfiling } from "bun-profiler";
// Fire-and-forget — call at app startup
startProfiling({
pyroscopeUrl: "http://localhost:4040",
appName: "my-service",
});For manual start/stop control:
import { BunPyroscope } from "bun-profiler";
const profiler = new BunPyroscope({
pyroscopeUrl: "http://localhost:4040",
appName: "my-service",
});
await profiler.start();
// Later, e.g. in tests or graceful shutdown:
await profiler.stop(); // flushes final profile before disconnectingConfiguration
| Option | Type | Default | Description |
|---|---|---|---|
| pyroscopeUrl | string | required | Pyroscope server URL |
| appName | string | SERVICE_NAME env / npm_package_name / "bun-app" | Application name |
| sampleIntervalUs | number | 10000 (10ms) | Sampling interval in microseconds |
| pushIntervalMs | number | 15000 (15s) | How often to flush profiles |
| labels | Record<string, string> | {} | Extra labels (merged with auto-detected) |
| authToken | string | — | Bearer token for auth |
| basicAuth | { username, password } | — | Basic auth credentials |
| maxRetries | number | 2 | Push retry attempts before dropping window |
| debug | boolean | false | Log debug info to stderr |
| wallTime | { enabled: boolean } | { enabled: false } | Wall-time profiling (opt-in) |
| heap | { enabled, samplingIntervalBytes? } | { enabled: false } | Heap allocation profiling (opt-in) |
Wall-time profiling
CPU profiling only captures on-CPU time — what your code does when it's actively executing JavaScript. For I/O-heavy servers that spend most time waiting on external APIs, databases, or network calls, CPU profiles miss the full picture.
Wall-time profiling weights stacks by elapsed wall-clock time (microseconds) and keeps (idle) frames visible, so you can see where wall-clock time actually goes — including I/O waits.
startProfiling({
pyroscopeUrl: "http://localhost:4040",
appName: "my-api-server",
wallTime: { enabled: true },
});When enabled, an additional wall profile stream is pushed alongside cpu. In Pyroscope/Grafana, select the my-api-server.wall{} stream to see the wall-time flamegraph.
Wall-time profiling adds no extra sampling overhead — it reuses the same CDP profile data as CPU profiling, just weights it by timeDeltas (actual elapsed microseconds per sample) instead of counting samples.
Heap profiling
Opt-in allocation profiling tracks where memory is being allocated:
startProfiling({
pyroscopeUrl: "http://localhost:4040",
appName: "my-service",
heap: { enabled: true, samplingIntervalBytes: 32_768 },
});When enabled, an alloc_space profile stream is pushed alongside cpu.
Bun limitation: Bun's JavaScriptCore runtime does not currently implement HeapProfiler.enable. When heap profiling is enabled on Bun, the profiler logs a warning and continues with CPU-only profiling. Heap profiling works on Node.js/V8. This will be supported once Bun adds HeapProfiler to their inspector implementation.
Auto-detected labels
The following labels are added automatically when the corresponding environment variables are set:
| Label | Environment variable(s) |
|---|---|
| service_name | SERVICE_NAME, npm_package_name, or appName option |
| service_version | SERVICE_VERSION, npm_package_version |
| environment | NODE_ENV, BUN_ENV |
| hostname | os.hostname() (always present) |
| fly_region | FLY_REGION |
| fly_app_name | FLY_APP_NAME |
| aws_region | AWS_REGION, AWS_DEFAULT_REGION |
| railway_region | RAILWAY_REGION |
| railway_service | RAILWAY_SERVICE_NAME |
| pod_name | POD_NAME |
| k8s_namespace | K8S_NAMESPACE |
Extra labels passed via the labels option override auto-detected values.
Local development
# Start Pyroscope
docker run -p 4040:4040 grafana/pyroscope
# Run checks (typecheck + lint + tests)
bun l
# Build
bun run buildHow it works
- Connects to Bun's embedded JavaScriptCore inspector via
node:inspector/promises - Every
pushIntervalMs: stops the profiler, converts the CDP profile to folded stacks, gzip-compresses it, and POSTs toPOST /ingest - Immediately restarts profiling — no gap in coverage
- On SIGTERM/SIGINT: flushes the current window before exiting
Why not Bun.jsc.profile()?
Bun exposes bun:jsc with a profile() function, but it's not suitable for continuous profiling:
- Wrong output format —
Bun.jsc.profile()returns aSamplingProfilewith pre-formatted text strings (.functions,.bytecodes,.stackTraces), not structured CDP/V8 JSON withnodes/samples/timeDeltas. There's no way to convert this to folded stacks without writing a brittle text parser. - No start/stop control —
Bun.jsc.startSamplingProfiler()has no corresponding stop function. It's a fire-and-forget debug tool that writes to a directory, not a programmatic API. node:inspectoralready works — Bun added fullnode:inspectorProfiler support in v1.3.7 (November 2024). This is the same CDP API that Chrome DevTools uses, returning properCdpProfileobjects that convert directly to Pyroscope's folded-stack format. That's why the minimum Bun version is 1.3.7.
If you're on Bun < 1.3.7, you'll get a clear error from start() explaining the requirement. Upgrade Bun and it works out of the box.
Graceful shutdown
Signal handlers are installed automatically. On SIGTERM or SIGINT, the profiler flushes the current window and disconnects before re-emitting the signal so your process exits normally.
Release
bun run release:patch # 0.1.0 → 0.1.1 (bug fixes)
bun run release:minor # 0.1.0 → 0.2.0 (new features)
bun run release:major # 0.1.0 → 1.0.0 (breaking changes)Bumps package.json, commits, tags, and pushes. GitHub Actions publishes to npm automatically via OIDC trusted publishing — no token required.
License
MIT
Built by mewc · ChartCastr
