Java Spring Boot – Reactive (WebFlux) vs. Blocking (Spring Web) – Kafka Consumers – A Quick Dive

Modern event-driven systems often rely on Kafka for high-throughput messaging. But how you consume these messages—using traditional blocking I/O or reactive non-blocking pipelines—can greatly impact performance and scalability. Let’s quickly break down the key differences through a real-world lens.

Table of Contents

  1. Setup Snapshot

  2. What Happens Inside the JVM?

  3. Key Learnings (Blocking vs. Reactive)

  4. What to Look For in Logs

  5. Spring Boot Actuator Differences

  6. Final Thought


Setup Snapshot

We tested both models by consuming Kafka trade events in a Spring Boot app:

  • Blocking Kafka Consumer: Uses @KafkaListener and Spring Web (spring-boot-starter-web)

  • Reactive Kafka Consumer: Uses Flux<ConsumerRecord> from Reactor and WebFlux (spring-boot-starter-webflux)

Both save data into PostgreSQL using JPA or R2DBC, depending on the model.


What Actually Happens Inside the JVM?

🔸 Blocking Path (Spring Web + JPA)

  • Each Kafka message is handled on a dedicated thread

  • That thread waits (blocks) for:

    • I/O (DB, network)

    • Locks

    • Internal queues

  • Thread pool grows under load

  • JVM shows more memory churn, increased GC activity

  • Higher pressure on CPU with context switching

🔸 Reactive Path (WebFlux + R2DBC)

  • Message flows through a reactive pipeline

  • Single thread handles many tasks (non-blocking)

  • DB calls are async via R2DBC

  • JVM shows stable thread count, low GC frequency

  • CPU works more predictably, even under heavy load


Key Learnings (Blocking vs. Reactive)

Topic Blocking (Spring Web) Reactive (WebFlux)
Thread Usage Many threads. Each request holds a thread while waiting. Fewer threads. Event loop-based, non-blocking execution.
Latency Higher under load. Blocked I/O delays response. Lower latency even under load due to async processing.
Resource Pressure High CPU and memory with concurrent load. Efficient CPU/memory. GC is calmer.
Kafka Message Handling Thread handles each message end-to-end. Split across async stages (subscribe, flatMap, etc).
Backpressure Needs explicit logic. Built-in via reactive streams.
Throughput Drops sharply if I/O slows. Stays stable, scales with demand.

What to Look For in Logs

When comparing both models, your logs tell a lot:

  • Blocking:

    • Threads idle or waiting

    • GC spikes during high throughput

    • Request time grows linearly with load

  • Reactive:

    • Fewer active threads

    • Messages processed in batches

    • Consistent latency and response size


Spring Boot Actuator Differences

Use http://localhost:8080/actuator endpoints (like /metrics, /health) to observe:

  • Blocking apps:

    • jvm.threads.live and system.cpu.usage spike under load

    • Slow DB calls visible in /metrics/jdbc.connections.active

  • Reactive apps:

    • Stable reactor.threads.executor.pool.size

    • Faster http.server.requests with smaller deviations

    • Better resilience under load visible via /metrics/reactor.*


Final Thought

If your service needs to scale with minimal resources, reactive wins. If you prioritize simplicity and already deal with I/O-bound code in small bursts, blocking may suffice.

Either way, understanding what happens under the hood—from threads to GC to logs—is what makes the difference between average and high-performing apps.