Node.js diagnostics_channel: Built-in Observability
Use the diagnostics_channel API to add zero-overhead observability to your Node.js applications without modifying library code.
What you'll learn
- ✓What diagnostics_channel is and how it works
- ✓Subscribing to built-in Node.js diagnostic events
- ✓Creating custom channels for your application
- ✓Building an APM-style tracer with TracingChannel
Prerequisites
- •Basic Node.js knowledge
- •Understanding of the event loop
- •Familiarity with HTTP modules
The diagnostics_channel module is Node.js’s answer to application-level observability. It provides a publish-subscribe mechanism that lets you instrument code without modifying it. Libraries publish diagnostic events, and monitoring tools subscribe to them. The overhead is near zero when no subscribers are active, making it safe to leave instrumentation in production code.
The problem it solves
Before diagnostics_channel, adding observability to a Node.js application meant one of two things: monkey-patching modules (fragile and error-prone) or requiring every library to integrate with a specific tracing SDK (unrealistic). diagnostics_channel provides a standard, low-level communication channel that decouples the code being observed from the code doing the observing.
Basic usage
A channel is identified by a string name. Publishers send messages to the channel, and subscribers receive them.
import diagnostics_channel from 'node:diagnostics_channel';
// Create or get a channel
const channel = diagnostics_channel.channel('my-app:request');
// Subscribe to the channel
channel.subscribe((message, name) => {
console.log(`[${name}] Request:`, message);
});
// Publish to the channel
channel.publish({
method: 'GET',
url: '/api/users',
timestamp: Date.now(),
});
When no subscribers are listening, channel.publish() is essentially a no-op. The message object is not even created if you check channel.hasSubscribers first:
if (channel.hasSubscribers) {
channel.publish({
// Only build this object when someone is listening
method: req.method,
url: req.url,
headers: Object.fromEntries(Object.entries(req.headers)),
});
}
Built-in channels
Node.js publishes diagnostic events on built-in channels. You can subscribe to these without any code changes to get visibility into HTTP requests, DNS lookups, and more.
HTTP client tracing
import diagnostics_channel from 'node:diagnostics_channel';
// Track all outgoing HTTP requests
diagnostics_channel.subscribe('http.client.request.start', (message) => {
const { request } = message;
console.log(`Outgoing request: ${request.method} ${request.host}${request.path}`);
});
diagnostics_channel.subscribe('http.client.response.finish', (message) => {
const { request, response } = message;
console.log(`Response: ${response.statusCode} from ${request.host}`);
});
Undici (fetch) tracing
Node.js’s built-in fetch uses Undici under the hood, which publishes its own diagnostic events:
diagnostics_channel.subscribe('undici:request:create', ({ request }) => {
console.log(`Fetch: ${request.method} ${request.origin}${request.path}`);
});
diagnostics_channel.subscribe('undici:request:headers', ({ request, response }) => {
console.log(`Response status: ${response.statusCode}`);
});
diagnostics_channel.subscribe('undici:request:error', ({ request, error }) => {
console.error(`Request failed: ${error.message}`);
});
Net and DNS
// Track TCP connections
diagnostics_channel.subscribe('net.client.socket', ({ socket }) => {
console.log(`New TCP connection to ${socket.remoteAddress}:${socket.remotePort}`);
});
// Track DNS lookups (Node.js 22+)
diagnostics_channel.subscribe('dns.lookup.complete', ({ hostname, addresses }) => {
console.log(`DNS resolved ${hostname} to ${addresses}`);
});
Creating custom channels
For your own application code, create channels that follow a namespacing convention:
// channels.js
import diagnostics_channel from 'node:diagnostics_channel';
export const channels = {
dbQuery: diagnostics_channel.channel('myapp:db:query'),
cacheHit: diagnostics_channel.channel('myapp:cache:hit'),
cacheMiss: diagnostics_channel.channel('myapp:cache:miss'),
authAttempt: diagnostics_channel.channel('myapp:auth:attempt'),
};
// db.js
import { channels } from './channels.js';
export async function query(sql, params) {
const start = performance.now();
try {
const result = await pool.query(sql, params);
if (channels.dbQuery.hasSubscribers) {
channels.dbQuery.publish({
sql,
params,
duration: performance.now() - start,
rowCount: result.rowCount,
});
}
return result;
} catch (error) {
if (channels.dbQuery.hasSubscribers) {
channels.dbQuery.publish({
sql,
params,
duration: performance.now() - start,
error: error.message,
});
}
throw error;
}
}
// monitoring.js — subscribe to everything
import { channels } from './channels.js';
channels.dbQuery.subscribe((message) => {
if (message.error) {
console.error(`DB ERROR [${message.duration.toFixed(1)}ms]: ${message.sql}`);
} else if (message.duration > 1000) {
console.warn(`SLOW QUERY [${message.duration.toFixed(1)}ms]: ${message.sql}`);
}
});
channels.cacheMiss.subscribe((message) => {
console.log(`Cache miss: ${message.key}`);
});
TracingChannel for async operations
TracingChannel is a higher-level API designed for tracing async operations. It creates a set of related channels for the lifecycle of an operation: start, end, asyncStart, asyncEnd, and error.
import diagnostics_channel from 'node:diagnostics_channel';
const tracing = diagnostics_channel.tracingChannel('myapp:http:request');
// The traced function
async function handleRequest(req, res) {
const user = await fetchUser(req.userId);
res.json(user);
}
// Wrap the function with tracing
const tracedHandler = tracing.tracePromise(async (req, res) => {
return handleRequest(req, res);
});
Subscribe to lifecycle events:
tracing.subscribe({
start({ input }) {
console.log('Request started');
input.startTime = performance.now();
},
end({ input, result }) {
const duration = performance.now() - input.startTime;
console.log(`Request completed in ${duration.toFixed(1)}ms`);
},
error({ input, error }) {
console.error(`Request failed: ${error.message}`);
},
asyncStart() {
console.log('Async work started');
},
asyncEnd() {
console.log('Async work completed');
},
});
Building a simple APM
Here is a minimal application performance monitoring setup using diagnostics_channel:
// apm.js
import diagnostics_channel from 'node:diagnostics_channel';
const metrics = {
httpRequests: 0,
dbQueries: 0,
slowQueries: 0,
errors: 0,
totalDbTime: 0,
};
// Track HTTP requests
diagnostics_channel.subscribe('http.server.request.start', () => {
metrics.httpRequests++;
});
// Track database queries
diagnostics_channel.subscribe('myapp:db:query', (message) => {
metrics.dbQueries++;
metrics.totalDbTime += message.duration;
if (message.duration > 500) {
metrics.slowQueries++;
}
if (message.error) {
metrics.errors++;
}
});
// Expose metrics endpoint
setInterval(() => {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
httpRequests: metrics.httpRequests,
dbQueries: metrics.dbQueries,
slowQueries: metrics.slowQueries,
avgDbTime: metrics.dbQueries > 0
? (metrics.totalDbTime / metrics.dbQueries).toFixed(1)
: 0,
errors: metrics.errors,
}));
// Reset counters
Object.keys(metrics).forEach((k) => (metrics[k] = 0));
}, 60_000);
Unsubscribing
Always unsubscribe when you no longer need to listen. This is especially important in tests:
const handler = (message) => {
console.log(message);
};
channel.subscribe(handler);
// Later...
channel.unsubscribe(handler);
Key takeaways
diagnostics_channel is the standard way to add observability to Node.js code. It has near-zero overhead when nobody is subscribing, making it safe for production. Subscribe to built-in channels for HTTP and DNS tracing without any code changes. Create custom channels for your application’s domain events. Use TracingChannel for structured async operation tracing. The separation between publisher and subscriber means you can add monitoring to any library without modifying its source code.
Related articles
- Node.js Node.js vs Deno vs Bun: JavaScript Runtimes Compared
Compare Node.js, Deno, and Bun on performance, security, compatibility, and developer experience. Choose the right JavaScript runtime for 2026.
- Node.js Node.js Cluster Module for Multi-Core Scaling
Scale your Node.js application across all CPU cores using the cluster module. Covers load balancing, zero-downtime restarts, and production patterns.
- Node.js Corepack: Managing Package Managers in Node.js
Use Corepack to enforce consistent package manager versions across your team. Covers setup, configuration, and CI integration.
- Node.js Node.js Built-in Test Runner: A Complete Guide to node:test
Learn how to use the native Node.js test runner with no external dependencies. Covers test organization, mocking, snapshots, and code coverage.