✅ Nonblocking Delay Link to heading
This is a small Reactor pattern for waiting on a condition without blocking threads. It is useful in startup orchestration and when you need to slow down calls to a downstream system. If you want a broader Reactor refresher, start with Reactor + Kotlin Intro.
🧭 The Problem Link to heading
Sometimes you’ll need to wait for a condition to be true — whether that’s waiting for a service to come online or trying to avoid overwhelming a remote system. Usually, you might think to do that with something like:
while (!ready()) {
Thread.sleep(1_000)
}
In reactive code, that’s not a great idea. Thread.sleep() is a blocking call, and in a nonblocking system like Reactor, that’s exactly what you’re trying to avoid. It ties up a thread that could be used for real work.
But the requirement doesn’t go away just because you’re working reactively — you still need to wait until the system is ready. So how do you do that the right way?
✅ The Pattern Link to heading
Here’s a clean way to do it:
// Somewhere in your reactive pipeline
.delayUntil { waitUntilReady() }
fun waitUntilReady(): Mono<Void> {
return Mono.defer {
ready()
.flatMap { isReady ->
if (isReady) {
Mono.empty()
} else {
Mono.delay(Duration.ofSeconds(10))
.then(waitUntilReady())
}
}
}
}
fun ready(): Mono<Boolean>
🧠 What’s Happening Here? Link to heading
This looks recursive, but it behaves like an iterative loop that schedules the next check after a delay.
ready()is a stub for your actual condition check — maybe you’re checking the health of a downstream service, queue depth, or some remote flag.delayUntilwaits until the publisher it is given completes. WhenisReadyistrue, we returnMono.empty(), which completes immediately and lets the pipeline continue.- When
isReadyisfalse, we insert a nonblockingMono.delay()and then return a newwaitUntilReady()to schedule the next check. - The entire process is wrapped in a
Mono<Void>, so it integrates cleanly into any Reactor pipeline. Mono.deferkeeps each evaluation lazy — nothing executes until the pipeline runs.
✅ When Should You Use This? Link to heading
This pattern is great when:
- You want to throttle requests to a remote system (e.g., RabbitMQ, Kafka, REST API) until it’s ready to handle them
- You’re doing startup orchestration, and one service depends on another coming online
- You’re waiting for a flag or resource state to change before proceeding
- You need to integrate polling behavior into a reactive stream without blocking any threads
💬 Example: Long Polling Chat Link to heading
Long polling over HTTP is a good fit for this pattern. A client issues a GET, the server holds the request up to a timeout, and then returns zero or more messages. The client reconnects immediately, which makes load balancing easier than long‑lived WebSocket connections.
Server‑side, the flow can look like this:
- Check for available messages
- If messages exist, return them and end the response
- If none exist, delay and check again until timeout
delayUntil lets you do this without tying up threads while you wait.
📧 Example: Email Throttling Link to heading
Imagine an email job that reads customers from the database, creates an email request, and sends it. If you are limited to 10,000 emails per hour, you can use delayUntil to pause when the limit is reached.
One simple approach:
- Track how many messages have been sent in the current hour
- When the hour flips, reset the counter
- When the limit is hit, delay before allowing the next send
The main risk here is prefetch. If the stream keeps reading while you are delayed, you can load far more customer records into memory than you need. Keep prefetch low enough to avoid memory spikes while the pipeline is waiting.
⚠️ Things to Watch Out For Link to heading
- This is recursive in shape but iterative in effect; Reactor keeps the stack safe because each step is async.
- If you’re checking something expensive or slow, consider moving the logic inside
ready()to a bounded elastic scheduler. - This pattern is best for short to moderate wait times. If you need something more robust (like exponential backoff or jitter), consider using
retryWhenorRepeat.
🏁 Wrap-Up Link to heading
Waiting for something doesn’t have to mean blocking. With Reactor, you can model time and conditions directly without tying up threads.