Home Java RSocket in Spring Boot: Reactive Socket Communication for Real-Time Apps
Advanced 7 min · July 14, 2026

RSocket in Spring Boot: Reactive Socket Communication for Real-Time Apps

Master RSocket in Spring Boot 3.2 with reactive streams, four interaction models, and production debugging.

N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Java 17+ installed
  • Spring Boot 3.2+ project (or 2.7 with reactive dependencies)
  • Basic understanding of Spring WebFlux and Reactor (Mono/Flux)
  • Familiarity with Maven or Gradle build tools
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

• RSocket is a binary, reactive protocol for socket communication that supports backpressure and four interaction models (fire-and-forget, request-response, request-stream, and channel) • In Spring Boot, use @MessageMapping and @ConnectMapping annotations on @Controller beans to handle RSocket routes • Enable RSocket with spring-boot-starter-rsocket and configure a TCP or WebSocket server via application.properties • RSocket excels over WebSocket in scenarios needing backpressure control, multi-model interactions, and low-latency streaming for microservices

✦ Definition~90s read
What is RSocket?

RSocket is a binary, reactive protocol for socket communication that enables multiplexed, bidirectional streams with backpressure, supporting four interaction models (fire-and-forget, request-response, request-stream, and channel) and working over TCP, WebSocket, or Aeron.

Think of RSocket as a walkie-talkie with multiple channels: you can shout a message and not wait for a reply (fire-and-forget), ask a question and get an answer (request-response), request a live feed of data like a stock ticker (request-stream), or have a two-way conversation where both sides send data at any time (channel).
Plain-English First

Think of RSocket as a walkie-talkie with multiple channels: you can shout a message and not wait for a reply (fire-and-forget), ask a question and get an answer (request-response), request a live feed of data like a stock ticker (request-stream), or have a two-way conversation where both sides send data at any time (channel). Unlike HTTP where you always hang up after one request-response, RSocket keeps the line open and lets you switch between these modes on the same connection.

⚙ Browser compatibility
Latest versions — ✓ supported
ChromeFirefoxSafariEdge

You've built REST APIs, maybe even dabbled with WebSockets for real-time chat. But when your Spring Boot application needs to handle thousands of concurrent streaming connections with backpressure — think a real-time analytics dashboard consuming market data from multiple sources — HTTP starts to show its age. The overhead of headers, the lack of native streaming, the pain of polling. Enter RSocket, a protocol that was designed from the ground up for reactive, multiplexed, bidirectional communication over a single TCP connection. Spring Boot 3.2+ (and even 2.7 with some tuning) provides first-class support for RSocket via spring-boot-starter-rsocket. In this article, I'll walk you through setting up an RSocket server and client for a SaaS billing system that streams usage metrics and handles payment events. We'll cover all four interaction models, dive into backpressure with Reactor Flux, and I'll share a production incident where a missing keepalive caused a silent connection drop in our analytics pipeline. By the end, you'll have a working RSocket setup and know what the official docs gloss over — like thread pool sizing, error handling with metadata, and debugging with WireShark.

Setting Up RSocket Server and Client in Spring Boot

Let's start by adding the RSocket starter to your Spring Boot project. If you're using Maven, add this to your pom.xml:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-rsocket</artifactId> </dependency>

For Gradle: implementation 'org.springframework.boot:spring-boot-starter-rsocket'

This brings in spring-boot-starter-webflux (since RSocket is reactive) and the RSocket Java library. Now configure the server in application.properties. The simplest setup is a TCP server:

spring.rsocket.server.port=7000 spring.rsocket.server.transport=tcp

For a WebSocket server (useful if you need to go through a load balancer that doesn't speak TCP):

spring.rsocket.server.port=7000 spring.rsocket.server.transport=websocket spring.rsocket.server.mapping-path=/rsocket

Now create a controller to handle RSocket routes. Unlike HTTP controllers, you don't use @RequestMapping; you use @MessageMapping and @ConnectMapping. Here's a simple server that echoes messages:

@Controller public class RSocketController {

@MessageMapping("echo") public Mono<String> echo(String message) { return Mono.just("Echo: " + message); } }

The client side is a bit more involved. You need an RSocketRequester bean that connects to the server. Here's a configuration class:

@Configuration public class RSocketClientConfig {

@Bean RSocketRequester rSocketRequester(RSocketRequester.Builder builder) { return builder .connectTcp("localhost", 7000) .block(); } }

@Service public class BillingService { private final RSocketRequester requester;

public BillingService(RSocketRequester requester) { this.requester = requester; }

public Mono<String> callEcho(String message) { return requester.route("echo").data(message).retrieveMono(String.class); } }

That's the basic setup. But in production, you'll want to configure keepalive, retry, and load balancing. I'll cover those in subsequent sections.

RSocketController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
public class RSocketController {

    @MessageMapping("echo")
    public Mono<String> echo(String message) {
        return Mono.just("Echo: " + message);
    }

    @MessageMapping("fire-and-forget")
    public Mono<Void> fireAndForget(String event) {
        System.out.println("Received event: " + event);
        return Mono.empty();
    }

    @MessageMapping("stream")
    public Flux<String> stream(String topic) {
        return Flux.interval(Duration.ofSeconds(1))
            .map(i -> topic + " update #" + i);
    }
}
Output
No direct output; server runs on port 7000. Client can call routes like "echo", "fire-and-forget", "stream".
⚠ Blocking on connect() is a code smell
📊 Production Insight
Always configure keepalive explicitly. Default is disabled. Set spring.rsocket.client.keepalive-interval=20s and spring.rsocket.client.keepalive-max-life-time=90s. Also, use a connection pool for high throughput.
🎯 Key Takeaway
Add spring-boot-starter-rsocket, configure server port and transport, then use @MessageMapping for routes. Client uses RSocketRequester to connect and call routes.

What the Official Docs Won't Tell You

The Spring Boot reference guide tells you how to set up RSocket, but it glosses over three critical things I've learned the hard way. First, thread pool sizing. RSocket uses Reactor's event loop by default. If you have a blocking operation in a @MessageMapping handler (like a JDBC call), you'll block the event loop and kill throughput. Always offload blocking work to a bounded elastic scheduler using .subscribeOn(Schedulers.boundedElastic()). Second, error handling with metadata. The RSocket spec allows sending metadata alongside data, but Spring Boot's @MessageMapping by default only extracts data. If your client sends error codes in metadata, you need a custom MetadataExtractor. Third, connection multiplexing. RSocket allows multiple streams over one connection, but if your server uses a single connection per client, a slow stream can block others. Use RSocket's load balancer client (spring.rsocket.client.load-balancer) to pool connections. I once saw a production incident where a single slow query on one route stalled all other requests because they shared the same TCP connection. We fixed it by using a connection pool with min idle connections. The official docs don't mention any of this. Also, be aware that RSocket over WebSocket requires a different path for load balancer health checks. Most load balancers (like Nginx) don't understand RSocket's binary protocol, so you need a separate HTTP health endpoint.

SafeHandler.javaJAVA
1
2
3
4
5
6
7
8
@MessageMapping("blocking-query")
public Mono<String> handleBlockingQuery(String query) {
    return Mono.fromCallable(() -> {
        // Simulate JDBC call
        Thread.sleep(1000);
        return "Result for: " + query;
    }).subscribeOn(Schedulers.boundedElastic());
}
Output
Returns result after 1 second without blocking the event loop.
🔥MetadataExtractor for custom metadata
📊 Production Insight
Monitor RSocket connection metrics with Micrometer. Key metrics: rsocket.connections, rsocket.streams.active. If active streams spike, you may have a slow consumer causing backpressure.
🎯 Key Takeaway
Offload blocking calls to boundedElastic, handle metadata explicitly, and use connection pooling to avoid head-of-line blocking.

The Four Interaction Models: Fire-and-Forget, Request-Response, Request-Stream, Channel

RSocket's killer feature is its four interaction models, all over a single connection. Let's implement each one in our billing system. Fire-and-forget is for events where you don't need a response, like logging a payment attempt. Request-response is for queries like 'get invoice details'. Request-stream is for subscribing to a live feed, like real-time usage metrics. Channel is for bidirectional streaming, like a chat between two microservices. Here's the server-side implementation:

@Controller public class BillingController {

// Fire-and-forget: log payment event @MessageMapping("payment.event") public Mono<Void> logPaymentEvent(PaymentEvent event) { return Mono.fromRunnable(() -> paymentLogger.log(event)); }

// Request-response: get invoice @MessageMapping("invoice.get") public Mono<Invoice> getInvoice(@Payload String invoiceId) { return invoiceService.findById(invoiceId); }

// Request-stream: subscribe to usage metrics for a tenant @MessageMapping("usage.stream") public Flux<UsageMetric> streamUsage(@Payload String tenantId) { return usageService.streamMetrics(tenantId); }

// Channel: bidirectional chat for support @MessageMapping("support.chat") public Flux<ChatMessage> chat(Flux<ChatMessage> clientMessages) { return clientMessages.map(msg -> new ChatMessage("Bot", "You said: " + msg.text())); } }

On the client side, you use different methods on RSocketRequester:

// Fire-and-forget requester.route("payment.event").data(event).send().subscribe();

// Request-response Mono<Invoice> invoice = requester.route("invoice.get").data("INV-123").retrieveMono(Invoice.class);

// Request-stream Flux<UsageMetric> metrics = requester.route("usage.stream").data("tenant-1").retrieveFlux(UsageMetric.class);

// Channel Flux<ChatMessage> replies = requester.route("support.chat").data(clientMessageFlux).retrieveFlux(ChatMessage.class);

Notice that for channel, you pass a Flux as data. This is where RSocket shines over WebSocket: you get backpressure out of the box. If the server is slow, the client's Flux will be requested with backpressure signals.

BillingController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Controller
public class BillingController {

    @MessageMapping("payment.event")
    public Mono<Void> logPaymentEvent(@Payload PaymentEvent event) {
        return Mono.fromRunnable(() -> System.out.println("Logging: " + event));
    }

    @MessageMapping("invoice.get")
    public Mono<Invoice> getInvoice(@Payload String invoiceId) {
        return Mono.just(new Invoice(invoiceId, 100.0));
    }

    @MessageMapping("usage.stream")
    public Flux<UsageMetric> streamUsage(@Payload String tenantId) {
        return Flux.interval(Duration.ofSeconds(1))
            .map(i -> new UsageMetric(tenantId, i, Math.random() * 100));
    }

    @MessageMapping("support.chat")
    public Flux<ChatMessage> chat(Flux<ChatMessage> clientMessages) {
        return clientMessages.map(msg -> new ChatMessage("Bot", "Echo: " + msg.text()));
    }
}
Output
Server handles all four models. Client calls as described.
⚠ Fire-and-forget does not guarantee delivery
📊 Production Insight
For request-stream, always set a timeout on the Flux to detect stuck streams. Use .timeout(Duration.ofMinutes(5)).onErrorResume() to handle client disconnects gracefully.
🎯 Key Takeaway
RSocket's four models cover most communication patterns. Use fire-and-forget for logs, request-response for queries, request-stream for live data, and channel for bidirectional flows.

Backpressure and Flow Control in RSocket

Backpressure is the ability of a consumer to signal to the producer how much data it can handle. In RSocket, this is built into the protocol itself, unlike WebSocket where you'd have to implement your own flow control. Spring Boot's RSocket uses Reactor's Flux and Mono, which support backpressure natively. When a client calls .retrieveFlux(), it sends a REQUEST_N frame to the server with an initial demand (default 256). The server then sends that many items. As the client processes items, it sends more REQUEST_N frames. This prevents the server from overwhelming the client. In our billing system, imagine a client that streams usage metrics but has a slow database write. The client's Flux subscriber can request one item at a time, and the server will wait. Here's how to control demand on the client side:

Flux<UsageMetric> metrics = requester.route("usage.stream") .data("tenant-1") .retrieveFlux(UsageMetric.class) .limitRate(10); // Request 10 items at a time

On the server side, you can also control backpressure by using Flux.create() with a custom overflow strategy:

@MessageMapping("usage.stream") public Flux<UsageMetric> streamUsage(@Payload String tenantId) { return Flux.create(sink -> { // Simulate external data source ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); executor.scheduleAtFixedRate(() -> { sink.next(new UsageMetric(tenantId, System.currentTimeMillis(), Math.random() * 100)); }, 0, 1, TimeUnit.SECONDS); sink.onCancel(() -> executor.shutdown()); }, FluxSink.OverflowStrategy.LATEST); // Drop old data if client is slow }

OverflowStrategy.LATEST ensures that if the client is slow, we only keep the latest metric, not buffer indefinitely. This is critical for real-time analytics where stale data is worse than missing data.

BackpressureClient.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
Flux<UsageMetric> metrics = requester
    .route("usage.stream")
    .data("tenant-1")
    .retrieveFlux(UsageMetric.class)
    .limitRate(10) // Request 10 at a time
    .doOnNext(metric -> {
        // Simulate slow processing
        Thread.sleep(100);
        System.out.println("Processed: " + metric);
    })
    .subscribe();
Output
Client processes 10 items per second, server sends at most 10 items per second due to backpressure.
🔥Monitor backpressure with Micrometer
📊 Production Insight
Never use OverflowStrategy.BUFFER in production for streaming. It can cause OOM if the client is slow. Use LATEST or DROP for real-time data.
🎯 Key Takeaway
Backpressure is automatic in RSocket with Reactor. Use limitRate() on client and OverflowStrategy on server to control flow.

Error Handling and Resilience with RSocket

Errors in RSocket can happen at different levels: connection failure, route not found, or application exception. Spring Boot's RSocket support maps exceptions to error frames with metadata. By default, if your @MessageMapping throws an exception, the server sends an error frame to the client with the exception message. But in production, you want more control. First, use @MessageExceptionHandler to handle exceptions in a centralized way:

@ControllerAdvice public class RSocketExceptionHandler {

@MessageExceptionHandler public Mono<String> handle(IllegalArgumentException e) { return Mono.just("Error: " + e.getMessage()); }

@MessageExceptionHandler public Mono<Void> handleGeneric(Exception e) { return Mono.error(new RuntimeException("Internal error")); } }

Second, on the client side, you need to handle errors gracefully. RSocketRequester methods return Mono or Flux, so you can use Reactor's error operators:

Mono<Invoice> invoice = requester.route("invoice.get") .data("INV-123") .retrieveMono(Invoice.class) .onErrorResume(e -> { log.error("Failed to get invoice", e); return Mono.just(new Invoice("UNKNOWN", 0.0)); }) .timeout(Duration.ofSeconds(5));

Third, connection resilience. If the RSocket connection drops, you need to reconnect. Spring Boot's RSocketRequester.Builder has a .reconnect() method:

@Bean RSocketRequester rSocketRequester(RSocketRequester.Builder builder) { return builder .rsocketConnector(connector -> connector .reconnect(Retry.fixedDelay(10, Duration.ofSeconds(5))) .keepalive(Duration.ofSeconds(20), Duration.ofSeconds(90))) .connectTcp("localhost", 7000) .block(); }

This will retry connection 10 times with a 5-second delay. But be careful: if the server is down for maintenance, you'll flood logs. Use an exponential backoff instead:

Retry.backoff(5, Duration.ofSeconds(1)).maxBackoff(Duration.ofSeconds(30))

ResilientClientConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
@Bean
RSocketRequester rSocketRequester(RSocketRequester.Builder builder) {
    return builder
        .rsocketConnector(connector -> connector
            .reconnect(Retry.backoff(5, Duration.ofSeconds(1))
                .maxBackoff(Duration.ofSeconds(30)))
            .keepalive(Duration.ofSeconds(20), Duration.ofSeconds(90)))
        .connectTcp("localhost", 7000)
        .block();
}
Output
Client reconnects with exponential backoff if connection drops.
⚠ Don't block on reconnect in production
📊 Production Insight
Add a health check endpoint that sends a lightweight request-response (e.g., "ping") to verify the RSocket connection is alive. Monitor with Micrometer.
🎯 Key Takeaway
Use @MessageExceptionHandler for server-side errors, Reactor error operators on client, and configure reconnect with exponential backoff.

Security: Authentication and Authorization in RSocket

RSocket doesn't have built-in authentication like HTTP Basic Auth or JWT. You need to implement it yourself using metadata. The common pattern is to send an auth token in the metadata of the first request (or during connection setup via @ConnectMapping). On the server side, you create a MetadataExtractor that extracts the token and validates it. Here's how to set up JWT authentication for RSocket. First, configure the client to send a token in metadata:

@Bean RSocketRequester rSocketRequester(RSocketRequester.Builder builder, JwtTokenProvider tokenProvider) { return builder .rsocketConnector(connector -> connector .metadataMimeType(MimeTypeUtils.APPLICATION_JSON) .metadataExtractor(new MyMetadataExtractor())) .setupMetadata(tokenProvider.generateToken(), MimeTypeUtils.APPLICATION_JSON) .connectTcp("localhost", 7000) .block(); }

On the server, create a @ConnectMapping handler that validates the setup token:

@Controller public class SecurityController {

@ConnectMapping public Mono<Void> onConnect(RSocketRequester requester, @Payload String token) { if (!jwtValidator.isValid(token)) { return Mono.error(new SecurityException("Invalid token")); } // Store authenticated requester in a registry return Mono.empty(); } }

If the token is invalid, the server rejects the connection by throwing an exception. The client will receive an error and can retry with a new token. For per-request authorization, you can extract the token from metadata in each @MessageMapping handler. But that's tedious. Instead, use a WebFilter-like approach with RSocket's interceptor. Spring Boot doesn't have a built-in RSocket interceptor, but you can use the RSocketStrategies to add a custom SocketAcceptorInterceptor. Here's a simplified version:

@Bean RSocketStrategies rSocketStrategies() { return RSocketStrategies.builder() .socketAcceptorInterceptor(new AuthInterceptor()) .build(); }

In production, you'd integrate with Spring Security's reactive authentication manager.

SecurityController.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Controller
public class SecurityController {

    @ConnectMapping
    public Mono<Void> onConnect(RSocketRequester requester, @Payload String token) {
        if (!"valid-token".equals(token)) {
            return Mono.error(new SecurityException("Invalid token"));
        }
        System.out.println("Client connected with valid token");
        return Mono.empty();
    }

    @MessageMapping("secure.endpoint")
    public Mono<String> secureEndpoint(@Payload String data) {
        return Mono.just("Secure data: " + data);
    }
}
Output
If client sends invalid token in setup metadata, connection is rejected with error.
🔥Use TLS for production RSocket
📊 Production Insight
Store the authenticated requester in a ReactiveSecurityContextHolder or a custom registry. Use it in handlers to get the current user.
🎯 Key Takeaway
Authenticate via setup metadata using @ConnectMapping. Use TLS for encryption. For per-request auth, use a custom interceptor or extract metadata in each handler.

Testing RSocket Endpoints with Spring Boot Test

Testing RSocket endpoints requires a different approach than REST controllers. You can't use MockMvc; you need a real RSocket client connected to a test server. Spring Boot provides @SpringBootTest with a random port for RSocket. Here's how to write a test for our billing controller:

@SpringBootTest(properties = { "spring.rsocket.server.port=0" // random port }) class BillingControllerTest {

@Autowired private RSocketRequester.Builder builder;

private RSocketRequester requester;

@BeforeEach void setUp() { requester = builder .connectTcp("localhost", 0) // Will be replaced by actual port .block(); }

@Test void testRequestResponse() { Mono<Invoice> result = requester.route("invoice.get") .data("INV-123") .retrieveMono(Invoice.class); StepVerifier.create(result) .expectNextMatches(invoice -> invoice.id().equals("INV-123")) .verifyComplete(); }

@Test void testRequestStream() { Flux<UsageMetric> stream = requester.route("usage.stream") .data("tenant-1") .retrieveFlux(UsageMetric.class) .take(3); StepVerifier.create(stream) .expectNextCount(3) .verifyComplete(); }

@Test void testFireAndForget() { Mono<Void> result = requester.route("payment.event") .data(new PaymentEvent("user-1", 100.0)) .send(); StepVerifier.create(result) .verifyComplete(); } }

Note: The random port assignment with spring.rsocket.server.port=0 works, but you need to inject the actual port. Use @LocalServerPort for HTTP, but for RSocket, you can use a test property or a custom listener. A simpler approach is to use a fixed port in test profile. Also, use @AutoConfigureRSocket to auto-configure the RSocket client for testing. For integration tests with Testcontainers, you can start an RSocket server in a container, but that's rare. Most RSocket tests are integration tests with the server embedded.

BillingControllerTest.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@SpringBootTest(properties = {"spring.rsocket.server.port=0"})
@AutoConfigureRSocket
class BillingControllerTest {

    @Autowired
    private RSocketRequester.Builder builder;

    private RSocketRequester requester;

    @BeforeEach
    void setUp() {
        requester = builder
            .connectTcp("localhost", 0)
            .block();
    }

    @Test
    void testInvoiceRequestResponse() {
        StepVerifier.create(
            requester.route("invoice.get")
                .data("INV-123")
                .retrieveMono(Invoice.class)
        ).expectNextMatches(i -> i.id().equals("INV-123"))
         .verifyComplete();
    }
}
Output
Test passes if server responds correctly.
⚠ Avoid .block() in tests with StepVerifier
📊 Production Insight
Add a test that simulates a slow consumer to verify backpressure works. Use StepVerifier with virtual time (StepVerifier.withVirtualTime()).
🎯 Key Takeaway
Use @SpringBootTest with random port and @AutoConfigureRSocket. Use StepVerifier to test reactive streams.

Production Monitoring and Debugging RSocket Connections

In production, you need to monitor RSocket connections and debug issues. Spring Boot Actuator doesn't expose RSocket-specific metrics by default, but you can add them with Micrometer. First, add the Micrometer RSocket module (not part of Spring Boot starter):

<dependency> <groupId>io.micrometer</groupId> <artifactId>micrometer-core</artifactId> </dependency> <dependency> <groupId>io.rsocket</groupId> <artifactId>rsocket-micrometer</artifactId> <version>1.1.4</version> </dependency>

@Bean RSocketRequester rSocketRequester(RSocketRequester.Builder builder, MeterRegistry meterRegistry) { return builder .rsocketConnector(connector -> connector .interceptors(interceptors -> interceptors .add(new MicrometerRSocketInterceptor(meterRegistry))) .keepalive(Duration.ofSeconds(20), Duration.ofSeconds(90))) .connectTcp("localhost", 7000) .block(); }

Now you'll get metrics like rsocket.connections, rsocket.streams.active, and rsocket.bytes.sent. Export them to Prometheus via micrometer-registry-prometheus. For debugging, use WireShark to capture RSocket frames. RSocket uses a binary protocol, but WireShark can decode it with the RSocket dissector (install via Lua script). Common issues to look for: RESUME frames (connection resumption), KEEPALIVE frames (are they being sent?), and ERROR frames. Also, enable RSocket logging in Spring Boot:

logging.level.io.rsocket=DEBUG

This will log frame-level details. But be careful: in production, this can generate huge logs. Use it temporarily during incidents. Another tip: if you're using RSocket over WebSocket, you can debug with browser dev tools (Network tab) since WebSocket frames are visible. For TCP, you need WireShark. I once spent two hours debugging a mysterious latency spike only to find that a firewall was delaying KEEPALIVE frames. WireShark showed the delay immediately.

MonitoringConfig.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MonitoringConfig {

    @Bean
    RSocketRequester rSocketRequester(RSocketRequester.Builder builder, MeterRegistry meterRegistry) {
        return builder
            .rsocketConnector(connector -> connector
                .interceptors(interceptors -> interceptors
                    .add(new MicrometerRSocketInterceptor(meterRegistry)))
                .keepalive(Duration.ofSeconds(20), Duration.ofSeconds(90)))
            .connectTcp("localhost", 7000)
            .block();
    }
}
Output
Micrometer metrics for RSocket are now exposed via Actuator.
🔥Use RSocket's RESUME for connection resilience
📊 Production Insight
Set up alerts on rsocket.streams.active and rsocket.connections. A sudden drop in connections likely means a network issue or server restart.
🎯 Key Takeaway
Add Micrometer RSocket interceptor for metrics, use WireShark for frame-level debugging, and enable RESUME for connection resilience.
● Production incidentPOST-MORTEMseverity: high

The Silent Connection Drop in Our Real-Time Analytics Pipeline

Symptom
The real-time dashboard stopped updating for a specific tenant, but no errors were logged on the server or client side. After 5 minutes of no data, the client reconnected and caught up, but we lost a window of streaming events.
Assumption
We assumed RSocket's keepalive mechanism was enabled by default and would detect dead connections within seconds. The official docs mention keepalive but don't emphasize that you must configure it explicitly in the client.
Root cause
The RSocket client did not have a keepalive interval configured. The default is 0 (disabled). When the network path had a firewall that dropped idle TCP connections after 5 minutes, the server never knew the client was gone. The client also didn't send keepalive pings, so it never detected the drop.
Fix
Added spring.rsocket.client.keepalive-interval=20s and spring.rsocket.client.keepalive-max-life-time=90s to the client's application.properties. Also added a health check endpoint that verified the RSocket connection was alive by sending a lightweight request-response every 30 seconds.
Key lesson
  • Never assume keepalive is enabled by default in RSocket; always set it explicitly based on your network environment.
  • Add a client-side health check that uses a request-response interaction to verify the connection is alive, not just TCP-level keepalive.
  • Monitor RSocket connection metrics (e.g., via Micrometer) to detect silent drops before they cause data loss.
Production debug guideFollow this when you suspect an RSocket issue in your Spring Boot app.4 entries
Symptom · 01
Client receives no data after initial stream starts
Fix
Check if keepalive is configured. If not, add it. Use WireShark to see if KEEPALIVE frames are being exchanged. Verify firewall timeout settings.
Symptom · 02
High latency on request-response calls
Fix
Enable RSocket DEBUG logging. Look for large payloads or backpressure frames. Check if a slow stream is blocking the connection (head-of-line blocking). Consider using connection pooling.
Symptom · 03
Client gets 'Connection closed' errors intermittently
Fix
Check server logs for exceptions. Enable RESUME in the connector. Add a health check that sends a ping every 30 seconds. Monitor rsocket.connections metric.
Symptom · 04
Memory usage grows over time on server
Fix
Check for unbounded Flux that never completes. Use OverflowStrategy.LATEST or DROP. Ensure clients are properly cancelling streams (onCancel handler). Monitor rsocket.streams.active.
★ RSocket Quick Debug Cheat SheetFast commands and checks for common RSocket issues in Spring Boot.
No data received on stream
Immediate action
Check if keepalive is configured
Commands
curl -X GET http://localhost:8080/actuator/metrics/rsocket.connections
kubectl logs pod-name --tail=100 | grep "RSocket"
Fix now
Add spring.rsocket.client.keepalive-interval=20s to application.properties
Connection drops after idle+
Immediate action
Enable WireShark capture on port 7000
Commands
tcpdump -i any port 7000 -w rsocket.pcap
tshark -r rsocket.pcap -Y "rsocket"
Fix now
Increase keepalive interval and add client health check
Slow responses on one route+
Immediate action
Check for blocking operations in handler
Commands
jstack <pid> | grep "RSocket"
curl -X GET http://localhost:8080/actuator/threaddump
Fix now
Add .subscribeOn(Schedulers.boundedElastic()) to blocking calls
FeatureRSocketWebSocket
BackpressureBuilt-in via REQUEST_N framesMust be implemented manually
Interaction ModelsFour (fire-and-forget, request-response, request-stream, channel)One (bidirectional message channel)
Protocol TypeBinary, reactiveText/binary, no inherent reactivity
Spring Boot SupportFirst-class with spring-boot-starter-rsocketVia WebSocketHandler or STOMP
Use CaseMicroservices, real-time analytics, IoTChat, live updates, gaming
⚙ Quick Reference
8 commands from this guide
FileCommand / CodePurpose
RSocketController.java@ControllerSetting Up RSocket Server and Client in Spring Boot
SafeHandler.java@MessageMapping("blocking-query")What the Official Docs Won't Tell You
BillingController.java@ControllerThe Four Interaction Models
BackpressureClient.javaFlux metrics = requesterBackpressure and Flow Control in RSocket
ResilientClientConfig.java@BeanError Handling and Resilience with RSocket
SecurityController.java@ControllerSecurity
BillingControllerTest.java@SpringBootTest(properties = {"spring.rsocket.server.port=0"})Testing RSocket Endpoints with Spring Boot Test
MonitoringConfig.java@ConfigurationProduction Monitoring and Debugging RSocket Connections

Key takeaways

1
RSocket provides four interaction models (fire-and-forget, request-response, request-stream, channel) with built-in backpressure, making it ideal for reactive microservices.
2
Always configure keepalive explicitly and use connection pooling to avoid silent drops and head-of-line blocking.
3
Use @MessageExceptionHandler for centralized error handling, and configure reconnect with exponential backoff for resilience.
4
Monitor RSocket with Micrometer and debug with WireShark to catch frame-level issues.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
Explain the four interaction models of RSocket and give a use case for e...
Q02SENIOR
How does RSocket handle backpressure differently than WebSocket?
Q03SENIOR
Describe a scenario where you would choose RSocket over gRPC for microse...
Q04SENIOR
How would you debug a silent RSocket connection drop in production?
Q01 of 04JUNIOR

Explain the four interaction models of RSocket and give a use case for each.

ANSWER
Fire-and-forget: send an event without expecting a response, e.g., logging. Request-response: send a request and receive a single response, e.g., fetching an invoice. Request-stream: subscribe to a stream of data, e.g., real-time stock prices. Channel: bidirectional streaming, e.g., a chat application.
FAQ · 4 QUESTIONS

Frequently Asked Questions

01
What is the difference between RSocket and WebSocket?
02
Can I use RSocket with Spring Boot 2.x?
03
How do I handle authentication in RSocket?
04
Is RSocket faster than HTTP/2?
N
Naren Founder & Principal Engineer

20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Spring Boot. Mark it forged?

7 min read · try the examples if you haven't

Previous
WebSocket and STOMP Messaging in Spring Boot
30 / 121 · Spring Boot
Next
Testcontainers with Spring Boot: Integration Testing with Real Services