Summary
On Node.js >=24.15.0 (which shipped a new native diagnostics_channel implementation), a process running dd-trace plus a normal set of instrumented libraries can register more than 1024 distinct channel names. Node's native implementation stores subscriber slots in a fixed-size array capped at kMaxChannels = 1024 and never reclaims an index, so allocating the 1025th distinct channel name aborts the process:
Assertion failed: (next_channel_index_) < (kMaxChannels)
dd-trace is by far the largest diagnostics_channel consumer in a typical app (hundreds of apm:* channels across instrumentations and plugins, plus tracingChannel expansions and per-AWS-service channel bags), so it is the practical trigger. Because it is a hard SIGABRT, it crash-loops any long-lived service. We hit it on a Temporal worker that lazily loads the AWS SDK / undici / other clients as it warms up.
Affected versions
- dd-trace-js: observed on 5.109.0; almost certainly all recent 5.x, as nothing about the mechanism is version-specific.
- Node.js: 24.15.0 -> 24.18.0, 25.x, and 26.x — every currently-released line that contains
src/node_diagnostics_channel.cc. Node 24.14.x and earlier are NOT affected (they use the old pure-JS diagnostics_channel with no cap). The Node-side fix (nodejs/node@594d0c5, "diagnostics_channel: grow native channel storage") is on main but unreleased in every line as of this writing.
Root cause
Node's native diagnostics_channel assigns every distinct string channel name a monotonically increasing native index that is never reclaimed, and caps the backing array at 1024:
// src/node_diagnostics_channel.h
static constexpr size_t kMaxChannels = 1024;
// src/node_diagnostics_channel.cc — GetOrCreateChannelIndex
auto it = channel_indices_.find(name);
if (it != channel_indices_.end()) return it->second; // same name reuses its slot
CHECK_LT(next_channel_index_, kMaxChannels); // aborts on the 1025th distinct name
uint32_t index = next_channel_index_++;
channel_indices_.emplace(name, index);
dd-trace's per-subscription channel allocation happens here:
// packages/dd-trace/src/plugins/plugin.js
class Subscription {
constructor (event, handler) {
this._channel = dc.channel(event) // one native channel slot per distinct `event` name
reached via plugin construction — e.g. PluginManager.loadPlugin -> new FetchPlugin(...) -> TracingPlugin constructor -> addTraceSubs() / addTraceSub() (packages/dd-trace/src/plugins/tracing.js) -> addSub() -> new Subscription(...). The instrumentation layer adds still more names (e.g. getChannelBag() in packages/datadog-instrumentations/src/aws-sdk.js creates 7 channels per AWS service suffix).
Channel names are static-per-integration, so this is not a leak of duplicate names — channels are memoized by name (both in the JS channels map and the native channel_indices_ map), and plugin construction is memoized per PluginManager. It is simply that the total distinct name count across all loaded dd-trace integrations (plus Node built-ins, undici, etc.) exceeds Node's fixed 1024 cap. A rough inventory in 5.109.0: ~461 static apm:* names + ~59 tracingChannel(...) sites (each expands to ~5 native channels) + per-AWS-service expansion, before counting non-dd-trace channels.
Representative stack
Assertion failed: (next_channel_index_) < (kMaxChannels)
... node::diagnostics_channel::BindingData::GetOrCreateChannelIndex(std::string const&)
... at Channel (node:diagnostics_channel)
... at channel (node:diagnostics_channel)
... at new Subscription (dd-trace/src/plugins/plugin.js)
... at Plugin.addSub (dd-trace/src/plugins/plugin.js)
... at TracingPlugin.addTraceSub (dd-trace/src/plugins/tracing.js)
... at new TracingPlugin (dd-trace/src/plugins/tracing.js)
... at new FetchPlugin
... at PluginManager.loadPlugin (dd-trace/src/plugin_manager.js)
(Also seen tipping over inside getChannelBag in datadog-instrumentations/src/aws-sdk.js — same root cause, different allocation site.)
Minimal repro
// node >=24.15.0, before the main-branch fix
const dc = require('diagnostics_channel');
for (let index = 0; index < 2000; index++) {
dc.channel(`repro:channel:${index}`); // each distinct name consumes one native slot
}
// The process SIGABRTs at the 1025th distinct name:
// Assertion failed: (next_channel_index_) < (kMaxChannels)
// Re-creating a channel with the same name does NOT count (indices are cached by name).
With dd-trace, the equivalent is reached organically: require('dd-trace').init() in a service that exercises enough integrations (AWS SDK across several services, undici/fetch, pg, redis, kafka, ...) crosses 1024 cumulative channels and aborts.
Expected vs actual
- Expected: dd-trace coexists with Node's
diagnostics_channel without terminating the process.
- Actual: on Node >=24.15.0, once cumulative distinct channel names exceed 1024, the next
dc.channel(name) aborts the process (SIGABRT), crash-looping the service.
Asks / possible mitigations
- Awareness plus a documented compatibility note for Node >=24.15.0 until the Node fix ships.
- Reduce channel-name pressure where feasible (avoid eagerly allocating channels for integrations/services that are never used; consolidate where names can be shared).
- Track the upstream fix (nodejs/node@594d0c5) landing in released lines.
Current workarounds: set DD_TRACE_DISABLED_PLUGINS to drop high-fanout integrations (e.g. fetch,aws-sdk) below the cap, or pin Node to 24.14.x until a Node release carries the storage-growth fix.
Summary
On Node.js >=24.15.0 (which shipped a new native
diagnostics_channelimplementation), a process running dd-trace plus a normal set of instrumented libraries can register more than 1024 distinct channel names. Node's native implementation stores subscriber slots in a fixed-size array capped atkMaxChannels = 1024and never reclaims an index, so allocating the 1025th distinct channel name aborts the process:dd-trace is by far the largest
diagnostics_channelconsumer in a typical app (hundreds ofapm:*channels across instrumentations and plugins, plustracingChannelexpansions and per-AWS-service channel bags), so it is the practical trigger. Because it is a hardSIGABRT, it crash-loops any long-lived service. We hit it on a Temporal worker that lazily loads the AWS SDK / undici / other clients as it warms up.Affected versions
src/node_diagnostics_channel.cc. Node 24.14.x and earlier are NOT affected (they use the old pure-JSdiagnostics_channelwith no cap). The Node-side fix (nodejs/node@594d0c5, "diagnostics_channel: grow native channel storage") is onmainbut unreleased in every line as of this writing.Root cause
Node's native
diagnostics_channelassigns every distinct string channel name a monotonically increasing native index that is never reclaimed, and caps the backing array at 1024:dd-trace's per-subscription channel allocation happens here:
reached via plugin construction — e.g.
PluginManager.loadPlugin->new FetchPlugin(...)->TracingPluginconstructor ->addTraceSubs()/addTraceSub()(packages/dd-trace/src/plugins/tracing.js) ->addSub()->new Subscription(...). The instrumentation layer adds still more names (e.g.getChannelBag()inpackages/datadog-instrumentations/src/aws-sdk.jscreates 7 channels per AWS service suffix).Channel names are static-per-integration, so this is not a leak of duplicate names — channels are memoized by name (both in the JS
channelsmap and the nativechannel_indices_map), and plugin construction is memoized perPluginManager. It is simply that the total distinct name count across all loaded dd-trace integrations (plus Node built-ins, undici, etc.) exceeds Node's fixed 1024 cap. A rough inventory in 5.109.0: ~461 staticapm:*names + ~59tracingChannel(...)sites (each expands to ~5 native channels) + per-AWS-service expansion, before counting non-dd-trace channels.Representative stack
(Also seen tipping over inside
getChannelBagindatadog-instrumentations/src/aws-sdk.js— same root cause, different allocation site.)Minimal repro
With dd-trace, the equivalent is reached organically:
require('dd-trace').init()in a service that exercises enough integrations (AWS SDK across several services, undici/fetch, pg, redis, kafka, ...) crosses 1024 cumulative channels and aborts.Expected vs actual
diagnostics_channelwithout terminating the process.dc.channel(name)aborts the process (SIGABRT), crash-looping the service.Asks / possible mitigations
Current workarounds: set
DD_TRACE_DISABLED_PLUGINSto drop high-fanout integrations (e.g.fetch,aws-sdk) below the cap, or pin Node to 24.14.x until a Node release carries the storage-growth fix.