Skip to content

Memory leak: frozen Nest Create Nest App span retains unbounded _sentryChildSpans (SDK ≥10.64; mitigated by openTelemetryBasicTracerProvider: true, which #22557 removes) #22860

Description

@m3Lith

Is there an existing issue for this?

How do you use Sentry?

Self-hosted/on-premise

Which SDK are you using?

@sentry/nestjs

SDK Version

10.66.0

Framework Version

NestJS 11

Link to Sentry event

No response

Reproduction Example/SDK Setup

SDK ≥10.64

Sentry.init({ debug: false, dsn: config.SENTRY_DSN, enabled: isPreproductionOrProduction(), enableMetrics: false, tracesSampleRate: 0, environment: config.NODE_ENV, release: version, })

Steps to Reproduce

  1. Nest app with @sentry/nestjs (SDK ≥10.64) + SentryModule.forRoot(), Sentry.init({ tracesSampleRate: 0 })
  2. Run under sustained HTTP / messaging load for ≥1h
  3. Force GC and compare old_space / take heap snapshots

Expected Result

  • An ended / frozen span should not keep accumulating children, or
  • _sentryChildSpans should be cleared / bounded when a span ends, or
  • Bootstrap / app_creation spans should not remain active parents after Nest boot completes

Actual Result

Ended Create Nest App span retains an unbounded _sentryChildSpans set for process lifetime → continuous Scope / SentryNonRecordingSpan growth even with tracesSampleRate: 0.

Additional Context

After upgrading from @sentry/nestjs@10.62.0, a long-lived Nest service’s JS heap grew ~80 MB/h under production traffic and never reclaimed on forced GC.

Heap snapshots ~ 70 minutes apart showed almost 1:1 growth of Scope and SentryNonRecordingSpan (~ +176k each). Retention was dominated by a single root span:

  • name: Create Nest App
  • op / attrs: app_creation.nestjs, sentry.origin=auto.http.otel.nestjs, nestjs.module=AppModule
  • _sentryChildSpans: ~154k direct children (of ~233k total child links in the heap)
  • Span also carried sentry.tracerProviderSpan (marker for spans from Sentry’s minimal tracer provider)

Important: this root span was already ended / frozen (_frozen: true, _status set). It still kept receiving children via addChildSpanToSpan into a strong Set (_sentryChildSpans), so memory grew for the lifetime of the process.

Observed constructor deltas (shallow, ~70 min):

Constructor Δ count Δ shallow
Object +1.09M +55 MB
Scope +176k +31 MB
Array +709k +22 MB
SentryNonRecordingSpan +176k +19 MB
WeakRef +528k +16 MB

Dominant retainer shape:

Scope ←_sentryScope— SentryNonRecordingSpan ←(Set)— _sentryChildSpans ← SentrySpan("Create Nest App")

Same growth pattern appeared on a second service after the same SDK bump, at a lower rate matching lower traffic — points away from app-specific code.

Suspected regression

10.64.0 introduced default use of Sentry’s minimal SentryTracerProvider instead of the full OTel BasicTracerProvider:

The leaking root is created by Nest instrumentation (getAppCreationSpanOptions / orchestrion NESTJS_APP_CREATIONstartInactiveSpan). Children are attached via addChildSpanToSpan in @sentry/core with no guard for an already-ended parent and no upper bound on _sentryChildSpans.

Related prior discussion (different trigger, same “never-pruned root span” failure mode): #13412
Upstream WeakRef work (#21242) is present in 10.66 but does not stop strong retention through _sentryChildSpans.

Workaround (confirmed on our prod)

Sentry.init({
  // ...
  openTelemetryBasicTracerProvider: true,
})

After deploying this, container memory on the affected Nest service returned to a flat profile.

The httpIntegration({ ignoreOutgoingRequests: () => true }) workaround from #13412 does not apply: the accumulator is the Nest app_creation span, not an outgoing HTTP/WebSocket span.

Note on the workaround's lifetime

openTelemetryBasicTracerProvider: true is currently our only mitigation. #22557 (merged to develop, not yet in a published release as of 10.69.0) removes this option along with SentrySpanProcessor / SentrySampler.

Once that lands, TypeScript will reject the option as unknown. Deleting it to silence the error silently reintroduces the leak, because initOtel no longer has an opt-out and the minimal SentryTracerProvider becomes the only path.

If the accumulation is inherent to SentryTracerProvider, removing the opt-out leaves affected Nest services with no mitigation short of disabling Sentry. Relevant to the direction in #22486.

Happy to provide redacted heap-snapshot excerpts / retainer dumps if needed.

Priority

React with 👍 to help prioritize this issue. Please use comments to provide useful context, avoiding +1 or me too, to help us triage it.

Metadata

Metadata

Assignees

Projects

Status
No status

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions