Skip to content

dd-trace registers >1024 diagnostics_channel channels, tripping Node's native kMaxChannels CHECK and aborting the process (Node >=24.15.0) #9568

Description

@fspinolo

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

  1. Awareness plus a documented compatibility note for Node >=24.15.0 until the Node fix ships.
  2. Reduce channel-name pressure where feasible (avoid eagerly allocating channels for integrations/services that are never used; consolidate where names can be shared).
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions