How do you use Sentry?
Self-hosted/on-premise
Version
2.56.0 (Python 3.12.6)
Steps to Reproduce
sentry_sdk.init with only LoggingIntegration + DedupeIntegration enabled.
- In a long-lived asyncio task: build a local variable of ~1 MB so the frame carries that
much data, raise ValueError, log with logger.exception(...), keep the task running.
- The exception, its traceback, and the 1 MB local stay alive for the lifetime of the task (and maybe after its death)
Expected Result
After logger.exception(...) returns and the except block exits, the exception, its
traceback, and all its frame locals become unreachable and memory are freed
Actual Result
The exception stays alive for the whole lifetime of the task. Memory grows with every new
captured exception on a new task, and the 1 MB local (plus the whole traceback chain)
stays pinned until the task itself ends.
RAM usage growth:

What I found
DedupeIntegration._last_seen is a ContextVar that stores the last captured exception.
The SDK tries to store it as weakref.ref(exc), but Python builtins (ValueError,
TypeError, KeyError, ...) do not support weakref. On TypeError the SDK falls back to
a strong reference
Under asyncio every long-lived task keeps its own ContextVar state, so the strong
reference lives as long as the task. Through that reference the SDK pins:
- the exception,
exc.__traceback__,
- every frame local in the traceback.
In our web crawler frame locals held response payloads in the 500 KB-1 MB range, which is where
the RSS growth came from
Fix (what I did)
Replace the dedupe processor with one that stores weakref.ref(exc) when possible and
silently skips on TypeError. Dedupe is lost for builtins, leak is gone
How do you use Sentry?
Self-hosted/on-premise
Version
2.56.0 (Python 3.12.6)
Steps to Reproduce
sentry_sdk.initwith onlyLoggingIntegration+DedupeIntegrationenabled.much data, raise
ValueError, log withlogger.exception(...), keep the task running.Expected Result
After
logger.exception(...)returns and theexceptblock exits, the exception, itstraceback, and all its frame locals become unreachable and memory are freed
Actual Result
The exception stays alive for the whole lifetime of the task. Memory grows with every new
captured exception on a new task, and the 1 MB local (plus the whole traceback chain)
stays pinned until the task itself ends.
RAM usage growth:

What I found
DedupeIntegration._last_seenis aContextVarthat stores the last captured exception.The SDK tries to store it as
weakref.ref(exc), but Python builtins (ValueError,TypeError,KeyError, ...) do not support weakref. OnTypeErrorthe SDK falls back toa strong reference
Under asyncio every long-lived task keeps its own
ContextVarstate, so the strongreference lives as long as the task. Through that reference the SDK pins:
exc.__traceback__,In our web crawler frame locals held response payloads in the 500 KB-1 MB range, which is where
the RSS growth came from
Fix (what I did)
Replace the dedupe processor with one that stores
weakref.ref(exc)when possible andsilently skips on
TypeError. Dedupe is lost for builtins, leak is gone