Repository navigation
Memory Leak Observed: SentryScheduleHook + Reactor Kafka Interaction #5051
Description
Integration
sentry-spring-boot-starter-jakarta
Java Version
21
Version
8.29.0
Steps to Reproduce
Summary
We observed a memory leak when using Sentry Java SDK with Reactor Kafka. The leak occurs even when idle (no messages), leading to OutOfMemoryError. The issue appears to stem from an interaction between the two libraries rather than either one alone.
Versions tested: Sentry 8.29.0 and 8.30.0 (same behavior)
Environment
- Java: OpenJDK 21.0.1 LTS
- Spring Boot: 3.5.9
- Reactor Kafka: 1.3.25
- OS: macOS
Our Analysis
Based on our investigation, we believe the issue arises from the following interaction:
The Mechanism
Reactor Kafka's ConsumerEventLoop.PollEvent reschedules itself infinitely on a dedicated single-threaded scheduler:
// reactor-kafka ConsumerEventLoop.java:310-412
class PollEvent implements Runnable {
public void run() {
consumer.poll(pollTimeout);
if (isActive.get()) {
schedule(); // Reschedules itself infinitely
}
}
void schedule() {
eventScheduler.schedule(this); // Triggers SentryScheduleHook
}
}Each schedule() call triggers SentryScheduleHook.apply(), which creates a forked scope with a parent reference:
// SentryScheduleHook.java
public Runnable apply(Runnable runnable) {
final IScopes newScopes = Sentry.getCurrentScopes().forkedCurrentScope("spring.scheduleHook");
return () -> { /* ... */ };
}
// Scopes.java:119 - each fork captures the parent
public IScopes forkedCurrentScope(String creator) {
return new Scopes(scope.clone(), isolationScope, globalScope, this, creator);
// ^^^^ parent reference
}Result: The dedicated scheduler thread is a permanent GC root, retaining an unbounded chain of parentScopes.
Why We Observed This Only with Reactor Kafka
We tested with Flux.interval() and other standard Reactor operations - no leak occurred. These use pooled schedulers where tasks complete and threads return to the pool, allowing scopes to be garbage collected.
Reactor Kafka differs by using a dedicated single-threaded scheduler (Schedulers.newSingle()) that:
- Never terminates while the receiver is active
- Reschedules the same
PollEventinfinitely - Acts as a permanent GC root retaining all forked scopes
GC Root Path (from heap dump)
Thread "reactive-kafka-{groupId}-1" [permanent GC root]
└─ SchedulerTask (82,915 instances after 17 min)
└─ ReentrantLock$NonfairSync
└─ io.sentry.Scope
└─ io.sentry.Scopes
└─ parentScopes → Scopes → parentScopes → ... (unbounded chain)
Test Results
| Scenario | Duration | ReentrantLock Growth | Rate/min | Heap Growth |
|---|---|---|---|---|
| App + Sentry 8.29.0 | 19 min | +84,349 | ~4,439 | +34 MB |
| Minimal repro + Sentry | 17 min | +69,758 | ~4,103 | +30 MB |
| App without Sentry (*) | 15 min | 0 | 0 | 0 MB |
(*) Manual GC performed before heap dumps to eliminate transient objects.
Reproduction
A minimal Spring Boot application is provided in the attached ZIP. See README.md for setup instructions.
./run-test.sh # Interactive test runner with 3 scenariosWorkaround
We disable SentryScheduleHook after Sentry initializes (see SentryWorkaroundConfiguration.kt):
@EventListener(ApplicationReadyEvent::class) // Must run AFTER Sentry's ApplicationRunner
fun disableSentryScheduleHook() {
Schedulers.onScheduleHook("sentry") { runnable -> runnable }
}Trade-off: Context propagation in scheduler tasks is disabled, but error reporting continues to work.
Sentry Version: 8.29.0, 8.30.0
Reactor Kafka Version: 1.3.25
Spring Boot Version: 3.5.9
We hope this analysis is helpful. We may have misunderstood something, so please let us know if you need additional information or clarification. Happy to provide heap dumps or run additional tests if needed.
sentry-reactor-kafka-leak-reproduction.zip
Expected Result
Expected: Memory should remain stable when the application is idle with no Kafka messages.
Actual Result
Actual: Continuous growth of these objects (observable in heap dumps):
java.util.concurrent.locks.ReentrantLock$NonfairSyncio.sentry.Scope/io.sentry.Scopesio.sentry.PropagationContext
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsNo status
Activity
linear commentedon Jan 22, 2026
JAVA-287
adinauer commentedon Feb 2, 2026
Thanks for reporting @harrypennont, we'll try to reproduce.
adinauer commentedon Feb 6, 2026
Thank you for your patience @harrypennont!
We're now excluding the
reactor.kafka.receiver.internals.ConsumerEventLoop$PollEventRunnableclass in the hook to skip forking SentryScopes. When testing locally I was no longer able to see the memory leak.We're planning to release this fix soon.
We're also talking about potential fixes that are more generic and apply to similar sources of this problem too but there's downsides too. For example, introducing some sort of scope cloning depth limit would silently break Sentry after a potentially long time of running the application.
harrypennont commentedon Feb 6, 2026
Hello @adinauer,
Thanks for the quick turnaround on this. We appreciate you fixing this specific case with ConsumerEventLoop$PollEvent. We'll be waiting for the release and will upgrade as soon as it's available.
adinauer commentedon Feb 6, 2026
There's a couple unmerged fixes, we wanna merge as well. We'll likely release at the beginning of next week.
adinauer commentedon Feb 9, 2026
@harrypennont https://github.com/getsentry/sentry-java/releases/tag/8.32.0 has just been released including the fix. Feedback would be great.
harrypennont commentedon Feb 10, 2026
Hi @adinauer,
Thanks for the release! I tested it and the memory leak seems resolved. Everything works perfectly.
adinauer commentedon Feb 11, 2026
Thank you for the feedback @harrypennont !