node:diagnostics_channel is a named publish-and-subscribe bus with one property that makes it usable on hot paths: publishing to a channel nobody listens to costs a property read. Libraries use it to expose timings, query text and retry counts without committing to a public API.
import dc from 'node:diagnostics_channel';
const cache = dc.channel('app:cache');
cache.subscribe(({ key, hit }) => console.log('cache ', key, hit ? 'HIT' : 'MISS'));
if (cache.hasSubscribers) cache.publish({ key: 'user:7', hit: false });
const db = dc.tracingChannel('app:db');
db.subscribe({
start: ({ sql }) => console.log('start ', sql),
asyncEnd: ({ result }) => console.log('asyncEnd', result ? `${result.rows} rows` : 'rejected'),
error: ({ error }) => console.log('error ', error.message),
});
const query = (sql) => db.tracePromise(async () => {
if (sql.startsWith('drop')) throw new Error('permission denied');
return { rows: 3 };
}, { sql });
await query('select * from users');
await query('drop table users').catch(() => {});cache user:7 MISS start select * from users asyncEnd 3 rows start drop table users error permission denied asyncEnd rejected
A TracingChannel bundles five channels under one name — start, end, asyncStart, asyncEnd and error — and tracePromise(), traceCallback() and traceSync() publish around a call. Every subscriber sees the same context object, so one can stamp a start time in start and read it back in asyncEnd, which fires for a rejection too.
Node publishes to built-in channels as well: 'http.server.request.start', 'net.client.socket' and the 'tracing:module.require:start' family let you watch the runtime itself, and dc.subscribe(name, fn) attaches by name without creating the channel first. The module and tracingChannel() are stable; every built-in channel is Stability 1 - Experimental, so pin the Node version when you build on one.