The rubric isn't dead! I just had to put it into "stasis" because of a very-low-latency but high-cognitive-cost event (and not only cognitive πŸ˜…): buying a house! But now that I'm back to being stable, let's restart with a technical deep dive into a silent killer of reactive programming!

πŸ“Œ Deep dive: anatomy of an (invisible) "Hot Loop"

A few days ago, on the project I'm assigned to, I watched my Mac turn into a little heater. The culprit? A "creative" handling of a Mono.empty().

Imagine a DB query that finds nothing and returns an empty Mono. The .repeat() sees that the flow has ended and re-subscribes immediately. The cycle repeats at the speed of light because there's no latency between one completion and the new subscription.

πŸ“Œ The numbers of the disaster

The test below reproduces exactly that scenario: four flatMaps in parallel on a Mono.empty() with .repeat(), for one second.

@Test
void test_repeatOnEmptyCausesHotLoop() {
    final AtomicInteger subscriptions = new AtomicInteger();
    final AtomicInteger onNext = new AtomicInteger();
    final Set<String> threads = ConcurrentHashMap.newKeySet();

    final String actual = Flux.range(0, 4)
            .flatMap(i -> Mono.<String>empty()
                    .doOnSubscribe(sub -> {
                        subscriptions.incrementAndGet();
                        threads.add(Thread.currentThread().getName());
                    })
                    .doOnNext(v -> onNext.incrementAndGet())
                    .repeat())
            .subscribeOn(Schedulers.parallel())
            .take(Duration.ofMillis(1000))
            .blockLast();

    assertNull(actual);
    System.out.println("Subscriptions in 1000ms (1s): " + subscriptions.get());
    System.out.println("onNext signals: " + onNext.get());
    System.out.println("Threads: " + threads);
    assertEquals(0, onNext.get(), "onNext signals");
}

Output:

Subscriptions in 1000ms (1s): 16059951
onNext signals: 0
Threads: [parallel-4, parallel-2, parallel-8, parallel-6]

In a single second, on 4 parallel threads, Project Reactor handled over 16 million subscriptions. The result? Zero elements produced (onNext signals: 0), but CPU at 100%.

πŸ“Œ The cost of "silence"

The real danger here is that there are no Exceptions. Nothing crashes. The system seems "just" very busy, while in reality it's running into a wall at a crazy speed.

If you use reactivity to handle retries or loops, always remember: if the starting point is empty, repeat() risks becoming a Hot Loop. A backoff (for example repeatWhen(...)) or the correct handling of a not-found can save you from a self-inflicted DoS attack.

Hoping another 3 months don't go by, see you at the next pill! β˜•