# 4. Back-pressure: the measurement that decides it Previous: [3. Payload](03-payload.md) · Next: [5. Choosing](05-choosing.md) --- Every protocol comparison eventually reduces to one question, and it is not throughput. It is: > **When the consumer stops keeping up, what happens?** [`BackPressureTest`](../src/test/java/com/ankurm/protocols/BackPressureTest.java) asks it the simplest way there is. Each server offers an unbounded stream. Each client takes a hundred messages and then does nothing. How many did the server produce? The sweep over quiet periods is what makes the answer conclusive. A number that stays flat as the wait grows means something bounded the producer. A number that keeps climbing means nothing did. [`backpressure.txt`](output/backpressure.txt): ``` protocol quiet 0.5s quiet 1s quiet 2s RSocket 100 100 100 gRPC 103,425 449,318 1,208,219 WebSocket 19,664 143,870 320,255 ``` RSocket produced **exactly one hundred**, at every quiet period. gRPC produced over a million in two seconds. Raw WebSocket, three hundred thousand. These are re-measured on every run and the absolute counts move by tens of percent; the three orders of magnitude between the rows do not. (The growth is faster than linear because the loop is still being JIT-compiled during the shortest run. The shape is the finding, not the exponent.) ## Why RSocket stops `request(n)` is a **frame on the wire**. When the client's subscriber calls `subscription.request(100)`, a REQUEST_N frame carries that number to the server, and Reactor's `Flux.generate` is called exactly a hundred times and then not again. Nothing blocks; there is no buffer to size and no thread parked. The producer simply is not invoked. That is what "Reactive Streams over the network" means, and it is the entire reason RSocket exists. The same property is why RSocket's streaming throughput in [chapter 2](02-benchmarks.md) is respectable rather than the highest: it is doing real work per batch that the others are not doing at all. ## Why the other two do not **Raw WebSocket has no mechanism.** The frame protocol has no notion of demand. The server writes until something underneath refuses — a socket buffer, Spring's `sendBufferSizeLimit`, the heap. Every one of those 320 255 messages was serialised and written for a client that had asked for a hundred. **gRPC has HTTP/2 flow control, and it did not help.** This is the result I did not expect. `StreamObserver.onNext` on a gRPC server **does not block**, so a plain server-streaming loop keeps producing regardless of what the client is doing. The documented answer is `ServerCallStreamObserver.isReady()`, and this module tests it rather than citing it ([`grpc-isready.txt`](output/grpc-isready.txt)): ``` handler quiet 0.5s quiet 2s plain onNext loop 258,145 1,421,778 isReady() checked before write 667,259 1,044,018 ``` **It did not bound the producer.** The `isReady()` row is not lower than the plain one in any useful sense — it produced 667 thousand messages in half a second for a client that wanted a hundred — and across runs it has come out both far above and far below the plain loop, which is its own kind of answer. I have not established whether the surplus accumulates in the server's outbound queue, in the client transport, or is discarded after the deframer, and I am not going to assert a mechanism I have not read the source for. What the numbers do establish is narrower and still worth knowing: **`isReady()` is not a demand signal**, and on a fast link with a stalled consumer it does not behave like one. If you are streaming from gRPC to a consumer that can fall behind, the bound has to come from your own design — a paged API, an explicit "send me more" message on a bidirectional stream, or a `count` in the request as `StreamQuotes` uses. ## The honest caveat A loopback connection is the worst case for this test in the fairest possible way: the wire drains instantly, so nothing downstream ever pushes back. On a slow real link, TCP and the HTTP/2 window *would* eventually stop the gRPC and WebSocket producers — by blocking a thread or by filling a buffer, but they would stop. That does not rescue them, because the failure mode you actually meet is not "the network is slow", it is "one consumer of forty is slow", and a shared server discovers that as latency for everyone else or as a heap that will not shrink. The difference RSocket makes is that the slow consumer's slowness is *expressed*, in a number, that the producer can act on before anything fills up. ## The other thing gRPC does not do for you Abandoning a gRPC stream produced this, repeatedly, in the server log: ``` java.lang.IllegalStateException: Failed to close the call Caused by: java.lang.IllegalStateException: call already closed ``` That is the cancellation story from [the Spring gRPC article](https://ankurm.com/spring-grpc-spring-boot-4/) showing up in practice: gRPC does not interrupt your thread when a client leaves, it sets a flag on the `Context`, and a handler that touches an already-closed stream throws. The loop in [`GrpcQuoteService`](../src/main/java/com/ankurm/protocols/GrpcQuoteService.java) checks `Context.current().isCancelled()` and returns without calling `onCompleted` for exactly this reason. --- Previous: [3. Payload](03-payload.md) · Next: [5. Choosing](05-choosing.md)