Debug any Node.js request like a timeline.
node-request-trace shows what happened inside a request, where the time went, and which step caused the slowdown, without asking you to adopt OpenTelemetry, run a collector, or wire up a tracing backend.
GET /checkout 340ms (200)
requestId: req_a1b2c3d4e5f6
bottleneck: dbQuery 210ms (61.8%)
coverage: 302ms traced (88.8%), 38ms uninstrumented
0ms |------------------------------------------------| 340ms
|- authMiddleware ## 12ms
|- validateInput # 5ms
|- dbQuery ############################## 210ms
`- paymentService ###### 45msOpenTelemetry is powerful, standard, and absolutely worth using for large production observability stacks. It is also a lot when you just want to answer one local question:
Why was this request slow?
node-request-trace is for that moment. Add middleware, wrap important work in trace.step(), and immediately get request timelines, bottleneck detection, N+1 detection, outgoing HTTP tracing, a browser dashboard, and exports for terminal reports, Markdown, Chrome tracing, and Speedscope.
const express = require("express");
const trace = require("node-request-trace");
trace.init({
slowThreshold: 200,
traceOutgoing: true,
});
const app = express();
app.use(trace.middleware());
app.use(trace.routes());
app.get("/checkout", async (req, res) => {
await trace.step("loadUser", () => users.find(req.query.userId));
await trace.step("chargeCard", () => payments.charge(req.body));
res.json({ ok: true });
});
app.listen(3000);Now open the dashboard:
open http://localhost:3000/trace/uiOr inspect the same request from the terminal:
npx node-request-trace timeline http://localhost:3000 req_a1b2c3d4e5f6- Request timelines with bottleneck, traced coverage, and uninstrumented gap detection.
- N+1 and duplicate-work detection within a single request.
- Async context propagation through
AsyncLocalStorage. - Manual
trace.step()spans for database calls, service calls, queues, cache reads, and any async work. - Automatic Express, Fastify, and Koa middleware/lifecycle timing.
- Automatic database and cache instrumentation for
pg,mongodb,ioredis,knex, and Prisma. - Outgoing
http,https, andfetchtracing. - Built-in dashboard at
/trace/uiwith waterfall, timeline, and flamegraph views. - CLI for stats, recent traces, slow traces, timeline reports, tailing, diffing, and exports.
- Trace diffing to catch regressions between two requests.
- Optional AI explanations of why a request was slow.
- Exports for Markdown, a self-contained shareable HTML snapshot, Chrome Trace Event Format, and Speedscope.
- Sampling, slow request detection, retention limits, and in-memory eviction.
- Header redaction by default and no request body capture.
- Zero runtime dependencies.
Use node-request-trace when you want a lightweight developer experience:
- You are debugging slow Node.js API requests.
- You want copy-pasteable trace output for PRs, issues, and incident notes.
- You do not want to run a collector, agent, backend, or dashboard service.
- You want local tracing now and can move to OpenTelemetry later if needed.
Use OpenTelemetry when you need distributed traces across many services, vendor integration, long-term retention, metrics/log correlation, or organization-wide observability standards.
node-request-trace is part of a small Node.js observability ecosystem you can adopt independently or together:
node-actuator-lite— Spring Boot-style/actuator/health,/info,/metrics,/env,/threaddump,/heapdump, and/prometheusendpoints.node-eventloop-watchdog— Detects event-loop stalls, captures stack traces and hotspots, and triggers recovery.node-request-trace— Per-request timelines, browser dashboard, and CLI without OpenTelemetry.
When all three are installed:
node-eventloop-watchdogautomatically registers/actuator/eventloop,/actuator/eventloop/history,/actuator/eventloop/hotspots, and/actuator/eventloop/metricsundernode-actuator-lite.- Event-loop block events include the active request id, route, and method captured by this tracer via
getCurrentRequestContext().
Runnable example: node-actuator-lite/examples/ecosystem.
Quickest setup: Use
node-observability-liteto wire the three packages together with production-safe presets in one line.const observability = require('node-observability-lite'); observability.express(app, { preset: 'production', auth: req => req.get('authorization') === `Bearer ${process.env.OPS_TOKEN}`, });
npm install node-request-traceThe package supports Node.js 16 and newer. It has optional peer support for Express, Fastify, and Koa, but no runtime dependencies of its own. AI explanations and fetch tracing use the global fetch, which is available on Node.js 18 and newer.
const express = require("express");
const trace = require("node-request-trace");
trace.init({
slowThreshold: 200,
samplingRate: 1,
traceOutgoing: true,
});
const app = express();
app.use(trace.middleware());
app.use(trace.routes());
app.get("/api/order/:id", async (req, res) => {
const order = await trace.step("db.order.find", () => db.orders.find(req.params.id));
const quote = await trace.step("shipping.quote", () => shipping.quote(order));
res.json({ order, quote });
});
app.listen(3000);const fastify = require("fastify")();
const trace = require("node-request-trace");
trace.init({ autoTrack: true });
fastify.register(trace.fastifyPlugin());
fastify.get("/api", async () => {
await trace.step("work", async () => {
// Your code here.
});
return { ok: true };
});const Koa = require("koa");
const trace = require("node-request-trace");
trace.init({ autoTrack: true });
const app = new Koa();
trace.instrumentKoa(app);
app.use(trace.koaMiddleware());
app.use(async (ctx) => {
await trace.step("work", async () => {
// Your code here.
});
ctx.body = { ok: true };
});Timeline reports are the main developer experience.
const report = trace.timeline(savedTrace);
console.log(report.summary.bottleneck);
console.log(trace.renderTimeline(savedTrace, { width: 80 }));Structured reports include:
steps: normalized request steps sorted by start time.summary.bottleneck: the slowest step.summary.topSteps: the top five slowest steps.summary.coveredDuration: time covered by recorded steps.summary.uninstrumentedDuration: request time not explained by recorded steps.summary.coveragePercent: how much of the request timeline is explained.summary.gaps: idle or uninstrumented windows between steps.summary.duplicates: repeated steps, including detected N+1 patterns.summary.errorCount: number of failed steps.
This makes it easy to answer:
- Did the database cause the slowdown?
- Did middleware run before the handler?
- How much of the request is still a blind spot?
- Am I making the same query in a loop?
- Which step should I optimize first?
Use trace.step(name, fn) around any async operation you care about.
const user = await trace.step("db.user.findById", async () => {
return db.users.findById(userId);
});Steps are recorded whether they succeed or fail. If the wrapped function throws, the error is attached to the step and then re-thrown so your application behavior stays the same.
Enable autoTrack to capture framework lifecycle and middleware timing.
trace.init({ autoTrack: true });Express records middleware layers. Fastify records lifecycle phases such as onRequest, preParsing, preValidation, handler, and onSend. Koa wraps app.use() middleware after trace.instrumentKoa(app).
Enable autoInstrument to time queries from common drivers without wrapping each call in trace.step().
trace.init({ autoInstrument: true });This patches pg (node-postgres), mongodb, and ioredis when they are present. Queries appear as db and cache steps with truncated, redaction-friendly labels:
pg SELECT * FROM users WHERE id = ? 12ms [db]
mongo users.findOne 8ms [db]
redis GET 1ms [cache]Limit which drivers are patched with the only list:
trace.init({ autoInstrument: { only: ["pg", "mongodb"] } });knex and Prisma are instance-based, so instrument them explicitly:
trace.instrumentKnex(knex);
trace.instrumentPrisma(prisma);You can also toggle driver instrumentation at runtime:
trace.enableAutoInstrumentation({ only: ["pg"] });
trace.disableAutoInstrumentation();Enable outgoing HTTP tracing to automatically capture calls made with Node's http, https, and global fetch.
trace.init({ traceOutgoing: true });Or toggle it manually:
trace.enableHttpTracing();
trace.disableHttpTracing();Outgoing calls are added as http-outgoing steps:
HTTP POST api.stripe.com/v1/charges 180ms [http-outgoing]
HTTP GET cdn.example.com/assets 45ms [http-outgoing]node-request-trace groups semantically identical steps within a request so repeated work stands out. Numbers, ids, UUIDs, and quoted literals are normalized, so SELECT ... WHERE id = 1 and SELECT ... WHERE id = 2 count as the same pattern.
const analysis = trace.analyze(savedTrace);
if (analysis.hasNPlusOne) {
console.log(analysis.nPlusOne);
}The analysis reports each repeated pattern with its count, total and average duration, whether it reads as an N+1 pattern, and how much time was wasted on repetition. Detected patterns also surface in timeline reports, the dashboard, Markdown exports, and AI explanations.
Compare two traces to find what changed between a fast request and a slow one, or before and after a deploy.
const diff = trace.diff(traceA, traceB);
console.log(diff.totalDeltaMs, diff.regressed);
console.log(trace.diffToMarkdown(traceA, traceB));Each step is labeled added, removed, slower, faster, or unchanged, with per-step deltas and an overall regression flag. From the CLI:
npx node-request-trace diff http://localhost:3000 req_aaa req_bbbTurn a trace into a focused, actionable explanation of why it was slow.
const text = await trace.explain(savedTrace, {
apiKey: process.env.OPENAI_API_KEY,
});The explanation uses any OpenAI-compatible chat completions endpoint. You supply the key, model, and base URL, so no vendor is bundled and no secrets are stored. Set OPENAI_API_KEY or LLM_API_KEY, or pass { apiKey } directly.
If you would rather not call a model, build the prompt and paste it into your own chat:
const { system, user } = trace.buildExplainPrompt(savedTrace);From the CLI, explain calls the model when a key is set, and otherwise prints the prompt for you to copy:
OPENAI_API_KEY=sk-... npx node-request-trace explain http://localhost:3000 req_abc123A trace can be exported in several formats for sharing and deeper analysis.
trace.toMarkdown(savedTrace); // GitHub-flavored markdown report
trace.toShareableHtml(savedTrace); // self-contained single-file HTML
trace.exportChromeTrace(savedTrace); // Chrome Trace Event Format object
trace.exportChromeTraceJson(savedTrace);
trace.exportSpeedscope(savedTrace); // Speedscope profile object
trace.exportSpeedscopeJson(savedTrace);- Markdown is ideal for PR comments, issues, and incident notes.
- HTML snapshot is a single self-contained file you can open in a browser or send to a teammate.
- Chrome Trace loads in
chrome://tracing. - Speedscope loads at speedscope.app for flamegraph analysis.
Mount trace.routes() and visit /trace/ui.
app.use(trace.routes());The dashboard includes:
- Recent request table.
- Slow request view.
- Live feed.
- Request detail view.
- Waterfall, timeline, and flamegraph modes.
- N+1 and duplicate-work warnings.
- Export buttons for Chrome trace, Speedscope, Markdown, and HTML snapshots.
The CLI talks to any app that has trace.routes() mounted.
npx node-request-trace stats http://localhost:3000
npx node-request-trace recent http://localhost:3000
npx node-request-trace slow http://localhost:3000
npx node-request-trace inspect http://localhost:3000 req_a1b2c3d4
npx node-request-trace timeline http://localhost:3000 req_a1b2c3d4
npx node-request-trace tail http://localhost:3000
npx node-request-trace diff http://localhost:3000 req_aaa req_bbb
npx node-request-trace markdown http://localhost:3000 req_a1b2c3d4
npx node-request-trace snapshot http://localhost:3000 req_a1b2c3d4 > trace.html
npx node-request-trace explain http://localhost:3000 req_a1b2c3d4
npx node-request-trace export http://localhost:3000 req_a1b2c3d4 > trace.json
npx node-request-trace speedscope http://localhost:3000 req_a1b2c3d4 > scope.jsonUse timeline when you want the fastest answer. Use inspect when you want the fuller detail view. Use diff to catch regressions, explain for an AI summary, and export, speedscope, markdown, or snapshot to take a trace elsewhere. markdown is also available as md.
| Endpoint | Description |
|---|---|
GET /trace/ui |
Browser dashboard |
GET /trace/recent |
Last 50 traces |
GET /trace/slow |
Slow traces using slowThreshold |
GET /trace/stats |
Aggregate latency, error, and throughput stats |
GET /trace/:requestId |
Raw trace JSON |
GET /trace/:requestId/timeline |
Timeline report plus ASCII rendering |
GET /trace/:requestId/chrome |
Chrome Trace Event JSON |
GET /trace/:requestId/speedscope |
Speedscope profile JSON |
GET /trace/:requestId/markdown |
GitHub-flavored Markdown report |
GET /trace/:requestId/snapshot |
Self-contained shareable HTML download |
GET /trace/diff/:idA/:idB |
Diff between two traces |
Example timeline response:
{
"requestId": "req_a1b2c3d4e5f6",
"method": "GET",
"path": "/checkout",
"totalDuration": 340,
"stepCount": 4,
"summary": {
"bottleneck": { "name": "dbQuery", "duration": 210, "percentOfRequest": 61.8 },
"coveredDuration": 302,
"uninstrumentedDuration": 38,
"coveragePercent": 88.8,
"errorCount": 0
},
"text": "GET /checkout 340ms (200)\\n..."
}Initializes tracing. Calling it again replaces the current configuration and storage.
trace.init({
slowThreshold: 200,
samplingRate: 1,
maxTraces: 1000,
retentionSeconds: 300,
autoTrack: false,
traceOutgoing: false,
autoInstrument: false,
sensitiveHeaders: null,
});Returns Express-compatible middleware by default. Pass "koa" to get Koa middleware.
Returns a Fastify plugin.
Returns Koa middleware.
Patches app.use() so Koa middleware can be timed automatically when autoTrack is enabled.
Returns middleware that serves the dashboard and JSON endpoints.
Records an async step on the current request trace and returns the wrapped function's result.
Returns the active request trace or null outside a traced request.
Builds a structured timeline report.
Builds an ASCII request timeline. Use options.width to control the chart width.
Returns N+1 and duplicate-work analysis for a trace's steps.
Compares two traces and returns a per-step diff with an overall regression flag.
Returns the diff as GitHub-flavored Markdown.
Calls an OpenAI-compatible endpoint and returns a short explanation of why the request was slow. Accepts { apiKey, model, baseUrl }.
Returns { system, user } prompt text so you can run the explanation in any chat tool without a key.
Patches supported drivers (pg, mongodb, ioredis) and returns the list that was applied. Accepts { only: [...] }.
Restores the original driver methods.
Attaches query timing to a knex instance via its event hooks.
Adds query timing through Prisma's $use middleware.
Supports "console", "pino", "winston", or a custom { onTrace(trace) {} } integration.
Patches outgoing http, https, and fetch calls.
Restores the original outgoing HTTP functions.
Returns Chrome Trace Event Format data, as an object or a JSON string.
Returns a Speedscope profile, as an object or a JSON string.
Returns a GitHub-flavored Markdown report for a trace.
Returns a self-contained HTML document for the trace.
Redacts sensitive headers using the configured list.
Stops cleanup timers, clears stored traces, disables HTTP tracing and auto-instrumentation, and resets the tracer.
| Option | Default | Description |
|---|---|---|
slowThreshold |
200 |
Duration in ms above which a request is marked slow |
samplingRate |
1 |
Fraction of requests to trace, from 0 to 1 |
maxTraces |
1000 |
Maximum number of traces stored in memory |
retentionSeconds |
300 |
How long traces stay in memory |
autoTrack |
false |
Enables framework middleware/lifecycle timing |
autoInstrument |
false |
Enables driver instrumentation; pass { only: [...] } to limit it |
traceOutgoing |
false |
Enables outgoing HTTP and fetch tracing |
sensitiveHeaders |
null |
Custom sensitive header list |
In development, run with full sampling and rich instrumentation:
trace.init({
samplingRate: 1,
autoTrack: true,
autoInstrument: true,
traceOutgoing: true,
});In production, keep the footprint smaller:
trace.init({
samplingRate: 0.05,
slowThreshold: 500,
maxTraces: 500,
retentionSeconds: 120,
});This library stores traces in memory. It is intentionally lightweight and local-first. For long-term retention or cross-service distributed tracing, export the data or use a full observability platform.
node-request-trace avoids sensitive data by default:
- Request and response bodies are not captured.
- Sensitive headers are redacted.
- Auto-instrumented query labels are truncated and strip literal values.
- The default redaction list includes
Authorization,Cookie,Set-Cookie,X-API-Key,X-Auth-Token, andProxy-Authorization.
Customize header redaction:
trace.init({
sensitiveHeaders: ["authorization", "x-internal-secret"],
});AI explanations are opt-in and only send the trace summary you generate. No key is bundled, and requests go directly to the endpoint you configure.
The library is designed to stay small:
- No runtime dependencies.
- In-memory
Mapstorage with bounded retention. - Optional sampling.
- Background cleanup timers use
unref()when available. - Unsampled requests skip tracing immediately.
| Tool | Best For | Tradeoff |
|---|---|---|
| OpenTelemetry | Standards-based distributed tracing across services | More setup and infrastructure |
| Logging libraries | Simple event logs | No request timeline or latency attribution |
node-request-trace |
Local-first request timelines and debugging | In-memory, not a full observability backend |
The goal is not to replace OpenTelemetry everywhere. The goal is to make request-level debugging fast, visual, and easy to adopt.
npm run exampleThen try:
curl http://localhost:3000/checkout
curl http://localhost:3000/fast
curl http://localhost:3000/error
open http://localhost:3000/trace/uinpm test
npm run test:coverageCoverage is enforced at 90% across lines, functions, branches, and statements.
The repository includes GitHub Actions for CI and release:
- CI runs lint, typecheck, package build, and coverage on Node 20 and 22.
- Release publishes to npm on
v*.*.*tags and creates a GitHub release.
MIT
