There's better ways than Monkey-Patching: A Path to Node.js Observability with Tracing Channels
Almost every production application uses a number of different tools and libraries. Be it a library to communicate with a database, a cache, or frameworks like Nest.js or Nitro. To be able to observe what’s going on in production, application developers reach out for Application Performance Monitoring (APM) tools like Sentry. But there’s an inherent problem: the performance data that APM tools need is most often not coming natively from the library itself. Getting this data is delegated to APM tools like Sentry or OpenTelemetry, which instrument crucial functionality of a library on their behalf.
What Is Instrumentation?
each of its components and used libraries. Instrumentation is the process of adding code to a program to monitor and analyze its internal operations and generate diagnostic data. It’s exactly what the Sentry SDKs and OpenTelemetry instrumentation are doing under the hood.
Consider a typical HTTP client library. Application developers want to know when a request starts and completes, along
with some metadata like URL, status code and headers. Today, libraries handle this inconsistently: some provide custom
hooks like emitter.on('request', ...), others offer vendor-specific middleware to intercept requests. In these cases,
Sentry and OpenTelemetry can write plug-ins that emit observability data.
This works, but it puts the burden on the library or framework (e.g. Nuxt) to consciously design an instrumentation API and identify the right places to expose it. Hooks and interceptors allow injecting observability code at the correct spots, but APM maintainers are entirely dependent on library authors to keep those APIs stable over time. On top of that, there is no shared convention (each library exposes different hook shapes and different metadata) so APM maintainers must write and maintain very different plugins for each library.
How server-side JavaScript is instrumented
The traditional approach to JavaScript instrumentation is “monkey-patching”. That’s modifying library code at runtime so that library functions not only do their original job, but also emit observability data. This is only possible in CommonJS (CJS), where modules are mutable and synchronously loaded.
However, the ecosystem is shifting. As server-side JavaScript moves further toward ES Modules (ESM), this approach breaks down. ES modules are immutable and loaded asynchronously, which means you simply can’t patch imports at runtime the same way anymore. For further information: the ESM Observability Instrumentation Guide covers this topic in greater detail.
The current workaround (and a way to “patch” imports) is using Module Customization Hooks paired with the --import
flag. A popular hook is import-in-the-middle/hook.mjs. It works, but it’s brittle, complex, and feels like what it is:
a workaround.
Both monkey-patching in CJS and Module Customization Hooks in ESM share the same fundamental flaw: they apply instrumentation “from the outside”. The library itself is passive. The question worth asking is: what if libraries were active participants in their own observability and emit telemetry data themselves? This would be possible through diagnostics APIs like Tracing Channels.
Libraries Should Emit Their Own Telemetry
Rather than waiting for APM tools to reach in and grab data, libraries can proactively expose their internal operations using tools built directly into the runtime. The right tool for this is Diagnostics Channels, and more specifically, Tracing Channels.
Diagnostics Channels
Diagnostics Channels are a high-performance, synchronous event system built directly into Node.js. They’re also supported in Bun, Deno, and Cloudflare Workers (via the Node.js compatibility flag), making them a cross-runtime primitive.
Their primary use case is one-off events. For example, “a connection was opened” (like
node-redis does this here).
The limitation is that they don’t inherently represent a full lifecycle. You have to manually link start and stop
events to measure duration.
Tracing Channels
Tracing Channels solve exactly that limitation. A Tracing Channel is a bundle of related Diagnostics Channels that
automatically creates sub-channels for a complete operation lifecycle: start, end, error, and asyncStart. More
importantly, a TracingChannel automatically propagates context across async boundaries. This means APM tools can
correlate a database query back to the incoming HTTP request that caused it, without any manual bookkeeping.
Together, they give library and framework authors a standardized way to expose internal operations without coupling to any specific logging or tracing vendor. The library emits structured events and observability tools decide what to do with them.
How Libraries Can Implement Tracing Channels
Tracing Channels have essentially zero cost when unused. If no subscriber is listening, emitting data costs almost
nothing. It means library authors can add tracing channels without worrying about penalizing users who don’t need
observability. The benefits are that there is no monkey-patching needed anymore and it eliminates the need for users to
pass --import flags for preloading in ESM.
Naming and Consistency: The Channel Is the Contract
Tracing Channels should always be scoped to the library that emits them, using the npm package name as the namespace.
Since package names are globally unique, this keeps channel names collision-free. For example, mysql2 ships
mysql2:query which would emit tracing:mysql2:query:start and all other channels. And the unstorage library ships
unstorage.get which emits tracing:unstorage.get:start and so on. The
untracing package is working to establish broader naming standards across the
ecosystem.
Equally important: Always emit a consistent data structure. Sentry and other APM tools can only provide automatic instrumentation if they know what shape your payload will have.
The pattern itself is straightforward. The library wraps its operation in a tracePromise call:
// Library side (e.g. inside ioredis)
import dc from "node:diagnostics_channel";
const commandChannel = dc.tracingChannel("ioredis:command");
// In the command execution path:
commandChannel.tracePromise(
async () => {
return await executeCommand(cmd);
},
{ command: cmd.name, args: cmd.args },
);
And on the consumer side, an SDK like Sentry subscribes to those events:
// Consumer side (e.g. Sentry SDK)
import dc from "node:diagnostics_channel";
dc.tracingChannel("ioredis:command").subscribe({
start(payload) {
// create span
},
asyncEnd(payload) {
// finish span
},
error({ error }) {
// record error
},
});
The library and the observability tool never need to know about each other. The channel is the contract.
The Ecosystem Is Already Moving
In early February 2026, we (Andrei, Jan and Sigrid) from Sentry attended OTel Unplugged EU and brought up the topic “Prepare for better JS ESM Support”, which was voted on the list of top priorities for the OpenTelemetry ecosystem.

So this isn’t a theoretical proposal. A growing number of well-known libraries have already shipped or merged PRs for Diagnostics Channel and Tracing Channel support.
On the framework and HTTP side, undici (Node.js’s built-in HTTP client)
has shipped Diagnostics Channels since Node 20.12, and
both fastify (docs) and
h3 (PR) have native support as well. On the database side,
mysql2 already uses Tracing Channels,
and pg / pg-pool are actively working on it. Redis clients aren’t far behind either and already support Tracing
Channels in ioredis (PR) and
node-redis (PR).
None of this happens without the people willing to do the work. A massive shoutout to Sentry engineer Abdelrahman Awad (@logaretm)
for driving Tracing Channel implementations across multiple libraries. And
a special thanks to Pooya Parsa (@pi0), his openness to collaborate in h3 was
instrumental in formalizing this approach and showing the ecosystem what it could look like.
The Vision Ahead
We’re still in a “chicken and egg” phase. Libraries need to add channels before APM tools have strong reasons to listen to them, and APM tools need to start listening before authors feel the pressure to add them.
The goal is universal JS observability: a world where Node.js, Bun, and Deno share the same diagnostic patterns, and
instrumentation just works without monkey-patching in CJS, without --import flags in ESM, and without fragile
workarounds. Libraries become active drivers of observability ensuring they are emitting data they think is the most
relevant to their users.