Skip to content

Memory Leak Observed: SentryScheduleHook + Reactor Kafka Interaction #5051

Closed

Description

@harrypennont

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 PollEvent infinitely
  • 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 scenarios

Workaround

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$NonfairSync
  • io.sentry.Scope / io.sentry.Scopes
  • io.sentry.PropagationContext

Activity

  1. linear commented on Jan 22, 2026

    @linear
  2. moved this to Waiting for: Product Owner in GitHub Issues with 👀 3on Jan 22, 2026
  3. adinauer commented on Feb 2, 2026

    @adinauer
    Member

    Thanks for reporting @harrypennont, we'll try to reproduce.

  4. moved this from Waiting for: Product Owner to No status in GitHub Issues with 👀 3on Feb 2, 2026
  5. adinauer commented on Feb 6, 2026

    @adinauer
    Member

    Thank you for your patience @harrypennont!

    We're now excluding the reactor.kafka.receiver.internals.ConsumerEventLoop$PollEvent Runnable class in the hook to skip forking Sentry Scopes. 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.

  6. harrypennont commented on Feb 6, 2026

    @harrypennont
    Author

    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.

  7. adinauer commented on Feb 6, 2026

    @adinauer
    Member

    There's a couple unmerged fixes, we wanna merge as well. We'll likely release at the beginning of next week.

  8. adinauer commented on Feb 9, 2026

    @adinauer
    Member

    @harrypennont https://github.com/getsentry/sentry-java/releases/tag/8.32.0 has just been released including the fix. Feedback would be great.

  9. harrypennont commented on Feb 10, 2026

    @harrypennont
    Author

    Hi @adinauer,
    Thanks for the release! I tested it and the memory leak seems resolved. Everything works perfectly.

  10. adinauer commented on Feb 11, 2026

    @adinauer
    Member

    Thank you for the feedback @harrypennont !

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions