Monkey-patching http.request or wrapping every pg.query leaks into behavior and breaks on library upgrades. Node’s diagnostics_channel lets you subscribe to named channels (undici:request:*, custom app channels) with near-zero overhead when no subscribers exist — production-safe instrumentation without patching.
⚡ TL;DR: Publish on stable channel names from shared clients; subscribe once at process boot for metrics/traces; keep subscribers sync and tiny; avoid capturing huge bodies. Pair with Node.js Event Loop Lag p99 and OpenTelemetry for LLMs.
Why monkey patches fail in production
Teams reach for Module.prototype wrappers or require-cache hijacks because they are familiar. In production those patches:
- Break when a library switches from
httptoundicior from callbacks to promises - Double-instrument under Nest/Express middleware stacks that already wrap the same APIs
- Capture full request bodies and leak PII into metrics backends
- Throw from inside a patched path and turn a slow query into a hard 500
Diagnostic channels invert the model: the library (or your facade) publishes; observers subscribe. If nobody is listening, publish is nearly free.
Publish from your client facade
// lib/db.ts
import dc from "node:diagnostics_channel";
const chQuery = dc.channel("acme:db:query");
const chError = dc.channel("acme:db:error");
export async function query<T>(sql: string, params: unknown[] = []) {
const start = performance.now();
try {
const rows = await pool.query(sql, params);
if (chQuery.hasSubscribers) {
chQuery.publish({
sql: sql.slice(0, 80), // ✅ bound size
ms: performance.now() - start,
rows: rows.rowCount,
});
}
return rows as T;
} catch (err) {
if (chError.hasSubscribers) chError.publish({ sql: sql.slice(0, 80), err });
throw err;
}
}
Keep the facade as the only place that talks to the pool. Feature code calls query(), never pool.query() directly — otherwise you reintroduce uninstrumented paths.
Subscribe once at boot
// instrumentation/db-metrics.ts
import dc from "node:diagnostics_channel";
dc.subscribe("acme:db:query", (msg: any) => {
// ✅ sync, allocation-light
metrics.histogram("db.query.ms", msg.ms, { bound: msg.sql });
});
dc.subscribe("acme:db:error", (msg: any) => {
metrics.incr("db.query.errors");
logger.warn("db_error", { sql: msg.sql, err: String(msg.err) });
});
Import instrumentation before the app listens. For undici, subscribe to undici:request:create / undici:request:headers rather than patching fetch — complements undici dispatcher pools.
Built-in channels worth wiring first
| Channel family | What you get | Typical metric |
|---|---|---|
undici:request:* |
Outbound HTTP timing | http.client.ms |
acme:db:query |
SQL latency + row counts | db.query.ms |
acme:cache:* |
Hit/miss + serialize cost | cache.op.ms |
acme:agent:tool |
Coding-agent tool calls | agent.tool.ms |
Document the catalog in-repo (docs/channels.md) with SemVer rules: additive fields OK; renaming a channel is a major bump.
Tracing bridge without OpenTelemetry monkey patches
// instrumentation/otel-bridge.ts
import dc from "node:diagnostics_channel";
import { trace, SpanStatusCode } from "@opentelemetry/api";
const tracer = trace.getTracer("acme-db");
dc.subscribe("acme:db:query", (msg: any) => {
const span = tracer.startSpan("db.query");
span.setAttribute("db.statement.preview", msg.sql);
span.setAttribute("db.duration_ms", msg.ms);
span.end();
});
dc.subscribe("acme:db:error", (msg: any) => {
const span = tracer.startSpan("db.query.error");
span.setStatus({ code: SpanStatusCode.ERROR, message: String(msg.err) });
span.end();
});
This keeps OTEL optional: if the bridge file is not imported in a given environment, channels still work for Prometheus-only setups. See OpenTelemetry for LLMs for span attribute conventions.
Test hygiene: subscribe and unsubscribe
import dc from "node:diagnostics_channel";
import { afterEach, test } from "node:test";
const seen: unknown[] = [];
function onQuery(msg: unknown) {
seen.push(msg);
}
test("query publishes timing", async () => {
dc.subscribe("acme:db:query", onQuery);
await query("select 1");
assert.equal(seen.length, 1);
});
afterEach(() => {
dc.unsubscribe("acme:db:query", onQuery);
seen.length = 0;
});
❌ Subscribing inside each request (leaks listeners across tests and warm Lambdas). ✅ Boot-time subscriptions with explicit unsubscribe in tests.
Rules that keep this safe
| Rule | Why |
|---|---|
Check hasSubscribers |
Zero cost when disabled |
| Sync subscribers only | Async work → separate queue |
| Bound payload size | No full SQL/PII bodies |
| Stable channel names | SemVer your channel catalog |
| No throws from subscribers | Errors must not break queries |
If a subscriber must do I/O (ship to a sidecar), push onto a bounded ring buffer and drain from a background interval — never await inside the subscribe callback.
Rolling out without a big bang
- Add facade publish behind
hasSubscribers(no observers yet) — ship - Add metrics subscribers in staging; compare cardinality vs patched baseline
- Flip production feature flag that imports
instrumentation/* - Delete monkey patches only after two weeks of matching dashboards
Closing checklist
✅ Dos
– ✅ Facade publish + boot subscribe
– ✅ hasSubscribers guards
– ✅ Bound, PII-safe payloads
– ✅ Prefer channels over monkey patches
– ✅ Document channel catalog next to metrics
– ✅ Unsubscribe in unit tests
– ✅ Bridge to OTEL optionally, not mandatorily
❌ Don’ts
– ❌ Don’t patch http/https globals in prod
– ❌ Don’t do I/O inside subscribers
– ❌ Don’t publish unbounded rowsets
– ❌ Don’t forget to unsubscribe in unit tests
– ❌ Don’t invent a new channel name per microservice clone
Related reading
- Node.js Event Loop Lag p99
- OpenTelemetry for LLMs
- Lambda Powertools Node Structured Logs
- Deterministic Replay: Agent Sessions
- Node undici Dispatcher Pools
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
