Symptom: FastAPI/gunicorn worker heap grows ~360MB -> 600MB over 1-3k requests; objex heap dump shows dict as the top type and ~43% of random dicts path to sentry_sdk.profiler.transaction_profiler.Profile.
Mechanics (sentry-sdk 2.66.1):
- Profile.write() appends one ProcessedSample dict PER THREAD per 101Hz tick, so a profile that runs to the 30s MAX_PROFILE_DURATION_NS cap in a worker with ~19 threads accumulates ~57k dicts (3030 ticks x 19), not 3030.
- Completed profiles stay pinned after the transaction finishes via Scope._profile on scope COPIES: (a) sentry's ThreadingIntegration wraps Thread.run with the creating request's isolation scope; a ThreadPoolExecutor pool thread's run() frame lives for the worker's lifetime, so every pool thread spawned during a profiled request pins that request's scope+profile forever; (b) asyncio loop._scheduled TimerHandle._context contextvars snapshots carry the scope the same way.
- profiles_sample_rate=1.0 with traces_sample_rate=1.0 means every request records a profile even if before_send_transaction filters the event send - the client-side sampling decision that gates profiling passes regardless of send filtering.
Diagnosis technique: in a dump whose heap is ~2x boot baseline, sample ~100 random objects of the dominant type and tally path-to-module prefixes; the leak owner names itself. Verify by summing the suspect's container lengths (16 Profiles held 279,511 sample dicts = the anomalous dict population almost exactly).
Fix options: lower profiles_sample_rate; or restart/recycle workers (pool threads die with the worker, releasing the pins).