You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Memory leak: contextWrapper on ServerResponse.close retains async context across keep-alive connections (Fastify) #20174
Sentry's contextWrapper is registered as a listener on ServerResponse._events.close. This event only fires when the underlying socket closes, not when the response finishes. With HTTP/1.1 keep-alive, the socket persists across requests, so every traced request's full async context, including database query results held in async function local variables (V8 Generator parameters_and_registers), is retained until the socket closes.
With nodeProfilingIntegration(), PROFILE_MAP (LRUMap) adds a second retention path that keeps scopes alive even after socket close.
Additional Context
Proposal: Clean up the contextWrapper listener when the response finishes (finish event or Fastify onResponse hook) rather than relying on socket close.
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.
Hi @s1gr1d, thanks for the scaffold — I worked from it and opened s1gr1d/sentry-js-20174-fastify-memory#1 with the pieces needed to surface the leak. Three gaps vs the setup in my report:
nodeProfilingIntegration() + profilesSampleRate: 1.0 was missing, so the PROFILE_MAP retention path never activated.
The synthetic array builder wasn't going through pg — I swapped it for a real Pool against Docker Postgres (docker-compose.yml bundled, pg-mem kept as fallback). The retention chain runs through pg.Connection callbacks patched by the OTel pg instrumentation, which never exist without a real wire protocol.
The handler was single-layer, so V8 flattened it — replaced with a handler → service → repository → driver chain to match the production call stack.
With all three, memlab find-leaks reports the following alive objects allocated during load (baseline → target → final, 2 × 15 × 50 requests, 10 keep-alive sockets, Node 22.15.1):
name
count
retainedSize
Object
162 668
130.5 MB
contextWrapper (closure)
18
124.4 MB
Connection (object)
9
62.2 MB
patchedClientConnectCallback
9
62.2 MB
patchedCallback (closure)
9
62.1 MB
cb (closure)
9
62.1 MB
Result (object)
9
62.1 MB
Socket
9
17 KB
9 pg Connections (pool size) each retain their last Result (~6.9 MB query response) through Connection → patchedClientConnectCallback → patchedCallback → cb → Result. The patched callbacks come from the OTel pg instrumentation installed by Sentry. 18 contextWrapper closures retain 124 MB total.
memlab technically reports 0 leaks because the retention plateaus at pool_size × last_Result, but the 124 MB held in contextWrapper and 62 MB held through the patched pg callbacks match the production trace one-for-one — heap grows 26 → 84 MB in round 1 and 84 → 99 MB in round 2 and does not release after forced GC.
Activity
linear-code commentedon Apr 9, 2026
JS-2106
s1gr1d commentedon Apr 10, 2026
Hi, thank you for filing this issue! We're taking a look at this.
s1gr1d commentedon Apr 13, 2026
Is it maybe possible for you to provide a small reproduction example regarding this?
I tried to create a reproduction with the help of Claude here: https://github.com/s1gr1d/sentry-js-20174-fastify-memory
However, I could not reproduce exactly your issue. Feel free to modify the reproduction.
Surface the leak: profiling + real pg + layered async
jerosa commentedon Apr 13, 2026
Hi @s1gr1d, thanks for the scaffold — I worked from it and opened s1gr1d/sentry-js-20174-fastify-memory#1 with the pieces needed to surface the leak. Three gaps vs the setup in my report:
nodeProfilingIntegration()+profilesSampleRate: 1.0was missing, so thePROFILE_MAPretention path never activated.pg— I swapped it for a real Pool against Docker Postgres (docker-compose.ymlbundled,pg-memkept as fallback). The retention chain runs throughpg.Connectioncallbacks patched by the OTel pg instrumentation, which never exist without a real wire protocol.handler → service → repository → driverchain to match the production call stack.With all three,
memlab find-leaksreports the following alive objects allocated during load (baseline → target → final, 2 × 15 × 50 requests, 10 keep-alive sockets, Node 22.15.1):ObjectcontextWrapper(closure)Connection(object)patchedClientConnectCallbackpatchedCallback(closure)cb(closure)Result(object)Socket9 pg Connections (pool size) each retain their last
Result(~6.9 MB query response) throughConnection → patchedClientConnectCallback → patchedCallback → cb → Result. The patched callbacks come from the OTel pg instrumentation installed by Sentry. 18contextWrapperclosures retain 124 MB total.memlab technically reports 0 leaks because the retention plateaus at
pool_size × last_Result, but the 124 MB held incontextWrapperand 62 MB held through the patched pg callbacks match the production trace one-for-one — heap grows 26 → 84 MB in round 1 and 84 → 99 MB in round 2 and does not release after forced GC.fix(opentelemetry): Use WeakRef for context stored on scope to preven…
fix(opentelemetry): Use WeakRef for context stored on scope to preven…
fix(opentelemetry): Use WeakRef for context stored on scope to preven…
github-actions commentedon Apr 16, 2026
A PR closing this issue has just been released 🚀
This issue was referenced by PR #20328, which was included in the 10.49.0 release.