Back to Reactive: Vert.x & RSocket
Q2Question 2 of 8

Explain backpressure in RSocket and how it differs from HTTP/2 (request(n) demand-based flow control at the application level).

Key Concepts
  • RSocket backpressure is demand-based at the message level via request(n), so the consumer controls exactly how many messages the producer sends.
  • HTTP/2 flow control uses a byte-window with WINDOW_UPDATE frames, protecting transport buffers but not application processing capacity.
  • RSocket carries Reactive Streams request(n) semantics across the wire as REQUEST_N frames, making it end-to-end pull-based.
  • HTTP/2 can fill its byte window with data that piles up in application buffers if the application is slower than the transport.
  • Never use request(Long.MAX_VALUE) on a stream you cannot keep up with, because that disables backpressure and invites memory exhaustion.
  • Fire-and-forget has no backpressure because there is no response stream, so use it only for best-effort telemetry.

Definition

RSocket backpressure is demand-based at the message level via request(n), while HTTP/2 flow control is byte-window-based at the transport level, so RSocket matches producer rate to what the application can actually process.
  • Backpressure – a mechanism by which a slow consumer limits how fast a producer sends data, preventing the consumer from being overwhelmed.
  • RSocket request(n) – the consumer issues request(n) to signal readiness for up to n messages; the producer must never emit more than the outstanding demand.
  • REQUEST_N frame – the wire-protocol frame that carries the consumer's demand from client to server.
  • HTTP/2 WINDOW_UPDATE – a sliding byte-window per stream and per connection, measured in bytes, that protects transport buffers but not application processing.
  • limitRate – a Reactor operator that automatically batches request(n) calls, keeping demand bounded and realistic.

Imagine National Bank of India streaming live stock prices to Rajesh's phone. Backpressure is Rajesh saying "only send me ten prices at a time; when I have shown those, I will ask for ten more." With RSocket, Rajesh controls the count of actual price updates he can display. HTTP/2 flow control is more like controlling the width of the water pipe in liters, not the number of glasses Rajesh can drink: it stops the pipe from overflowing the plumbing but does not know that Rajesh's screen can only render ten prices per refresh.

Understanding the Concepts

What is backpressure?

Backpressure is the mechanism by which a slow consumer limits how fast a producer sends data, preventing the consumer from being overwhelmed with more work than it can handle. Without backpressure, a fast producer can flood a slow consumer, causing memory exhaustion, dropped messages, or crashes.

JAVA
package com.bank.rsocket;

import reactor.core.publisher.Flux;
import java.math.BigDecimal;
import java.time.Duration;

public class BackpressureNeed {
    public static void main(String[] args) {
        // Fast producer: 1000 prices per second
        Flux<BigDecimal> priceFeed = Flux.interval(Duration.ofMillis(1))
                .map(i -> new BigDecimal("2450.00")
                        .add(BigDecimal.valueOf(i)));

        // Slow consumer: can only process 10 per second
        priceFeed
                .limitRate(10)  // backpressure: request only 10 at a time
                .delayElements(Duration.ofMillis(100)) // simulate slow rendering
                .doOnNext(price -> System.out.println("Rendered: " + price))
                .take(5)
                .blockLast();
    }
}

How does RSocket implement backpressure?

RSocket carries Reactive Streams request(n) semantics across the network. When a consumer subscribes to a request-stream or request-channel, the REQUEST_N frame communicates the consumer's demand to the remote producer.

JAVA
package com.bank.rsocket;

import io.rsocket.RSocket;
import io.rsocket.util.DefaultPayload;
import reactor.core.publisher.Flux;

public class RSocketBackpressure {
    public void streamPrices(RSocket rSocket) {
        rSocket.requestStream(DefaultPayload.create("RELIANCE"))
                .limitRate(16)  // request 16 at a time
                .map(payload -> payload.getDataUtf8())
                .doOnNext(price -> System.out.println(
                        "Rajesh's phone received: " + price))
                .subscribe();

        // What happens on the wire:
        // 1. Client sends REQUEST_STREAM with initial request(16)
        // 2. Server sends 16 Payload frames
        // 3. Client processes them, then sends REQUEST_N(16)
        // 4. Server sends 16 more
        // The slow side dictates the rate.
    }
}
DIAGRAM
Wire-level flow:
  Client ──REQUEST_STREAM(n=16)──▶ Server
  Client ◀──PAYLOAD(1)──────────── Server
  Client ◀──PAYLOAD(2)──────────── Server
  ...    ◀──PAYLOAD(16)─────────── Server
  Client ──REQUEST_N(n=16)───────▶ Server
  Client ◀──PAYLOAD(17)─────────── Server
  ...

How does HTTP/2 flow control work?

HTTP/2 uses a sliding window of bytes per stream and per connection. The receiver advertises available window space with WINDOW_UPDATE frames, and the sender may only transmit as many bytes as the window allows.

DIAGRAM
HTTP/2 flow control (byte-based):
  Client window: 65535 bytes
  Server sends 32000 bytes → window = 33535
  Server sends 32000 bytes → window = 1535
  Server blocks (window exhausted)
  Client sends WINDOW_UPDATE(65535) → window = 67070
  Server resumes sending

The key difference: HTTP/2 measures bytes, not application messages. A server can fill the HTTP/2 window with data that then piles up in application buffers if the application is slower than the transport.

How do RSocket and HTTP/2 flow control compare?

| Feature | RSocket | HTTP/2 |
|---|---|---|
| Unit of control | Application messages | Bytes |
| Mechanism | request(n) / REQUEST_N frame | WINDOW_UPDATE frame |
| Level | Application level | Transport level |
| Awareness | Knows the app can process n messages | Knows the buffer can hold n bytes |
| Pull-based | Yes, end to end | Only at the transport layer |
| Streaming | First-class with backpressure | Streaming exists but no app-level demand |

JAVA
package com.bank.rsocket;

public class ComparisonDemo {
    // RSocket: consumer controls message count
    // "Send me 16 price ticks. I will ask for more when ready."
    // Server sends exactly 16 ticks and waits.

    // HTTP/2: transport controls byte window
    // "My buffer has 65535 bytes of space."
    // Server can send 65535 bytes of price data.
    // If each tick is 100 bytes, that is 655 ticks.
    // But Rajesh's phone can only render 10 per second.
    // The 645 excess ticks pile up in application memory.
}

What are common mistakes with backpressure?

The most dangerous mistake is requesting an unbounded amount on a stream you cannot keep up with.

JAVA
package com.bank.rsocket;

import io.rsocket.RSocket;
import io.rsocket.util.DefaultPayload;

public class BackpressureMistakes {

    // WRONG: request(Long.MAX_VALUE) disables backpressure
    public void unboundedRequest(RSocket rSocket) {
        rSocket.requestStream(DefaultPayload.create("RELIANCE"))
                .subscribe(); // default request is Long.MAX_VALUE!
        // The server floods the client, memory exhaustion follows
    }

    // CORRECT: use limitRate to keep demand bounded
    public void boundedRequest(RSocket rSocket) {
        rSocket.requestStream(DefaultPayload.create("RELIANCE"))
                .limitRate(16)  // request 16 at a time
                .doOnNext(payload -> process(payload))
                .subscribe();
    }

    private void process(io.rsocket.Payload payload) {
        System.out.println("Processing: " + payload.getDataUtf8());
    }
}

What interaction models support backpressure?

Not all four RSocket interaction models have backpressure.

  • Request-response – single message, no streaming backpressure needed.
  • Fire-and-forget – no response stream, no backpressure at all.
  • Request-stream – full request(n) backpressure on the response stream.
  • Request-channel – full request(n) backpressure in both directions.
JAVA
// Fire-and-forget: no backpressure, no delivery guarantee
rSocket.fireAndForget(DefaultPayload.create("audit-event"));
// Use only for best-effort telemetry, NEVER for money movement

What about gRPC backpressure?

With gRPC over HTTP/2, you get HTTP/2 byte-window flow control, and gRPC's reactive bindings can propagate some backpressure, but the fundamental control is transport-level. RSocket's demand signal is the application's own request(n) count.

JAVA
// gRPC: ServerCallStreamObserver provides isReady() and setOnReadyHandler()
// but the underlying control is HTTP/2 byte windows

// RSocket: demand is end-to-end at the message level
rSocket.requestStream(DefaultPayload.create("RELIANCE"))
        .limitRate(16) // exactly 16 messages at a time
        .subscribe();

What should a senior engineer know?

Backpressure is cooperative: a misbehaving producer that ignores demand breaks the guarantee. Always rely on well-behaved Reactive Streams implementations on both ends. In banking, bounded demand on a price stream is what keeps a surge in market volatility from crashing thousands of mobile clients.

  • Use limitRate or a bounded subscriber to keep demand realistic.
  • Monitor demand with metrics to detect mismatched producer/consumer speeds.
  • Fire-and-forget has no backpressure because there is no response stream.
  • In production, tune the prefetch size based on the consumer's actual processing capacity.

Visual Overview

DIAGRAM
  RSocket Backpressure (Message-Level):
  ┌──────────┐  request(16)  ┌──────────┐
  │  Client  │──────────────▶│  Server  │
  │ (Rajesh) │               │ (Bank)   │
  │          │◀──16 prices───│          │
  │          │               │ (waits)  │
  │ process  │               │          │
  │          │──request(16)─▶│          │
  │          │◀──16 prices───│          │
  └──────────┘               └──────────┘
  Consumer controls the rate in messages.

  HTTP/2 Flow Control (Byte-Level):
  ┌──────────┐  window=65535  ┌──────────┐
  │  Client  │               │  Server  │
  │          │◀──32000 bytes──│          │
  │          │◀──32000 bytes──│          │
  │          │  window=1535   │ (blocks) │
  │          │──WINDOW_UPDATE─▶│          │
  │          │◀──more bytes───│          │
  └──────────┘               └──────────┘
  Transport controls the rate in bytes.
  App may still be overwhelmed.

Key Takeaway

Backpressure is how a slow consumer limits how fast a producer sends data so the consumer is never overwhelmed. RSocket implements it at the application level using Reactive Streams semantics. The consumer calls request(n) to say it is ready for up to n more messages, that demand is carried across the connection as a REQUEST_N frame, and the producer is contractually forbidden from emitting more than the outstanding demand. The slow side dictates the rate, and the unit of control is application messages the consumer is actually ready to process.

HTTP/2 also has flow control, but it is a different thing in both unit and purpose. It uses a sliding window of bytes per stream and per connection, advertised with WINDOW_UPDATE frames. It protects the transport buffers from being overrun, but it is measured in bytes, not application messages, and it has no idea whether the application has actually consumed a logical message. A server can keep filling the HTTP/2 byte window while data piles up in the application's own buffers.

At National Bank of India this difference is concrete. During volatile markets the price engine can emit thousands of Reliance ticks per second, but Rajesh's phone can only render a handful. With RSocket, his client requests a small batch, renders it, then requests more, and that demand is relayed over the wire so the server paces itself to exactly what the phone can display. The key discipline is to never request an unbounded amount on a stream you cannot keep up with, because that disables backpressure and invites memory exhaustion.

Remember one sentence: RSocket backpressure is demand-based at the message level through request(n), while HTTP/2 flow control is byte-window-based at the transport level, which means RSocket matches the producer's rate to what the application can truly process.

---

Share
Use ← → arrow keys to navigate