protocol-comparison — RSocket vs gRPC vs WebSocket, same use case, measured
Companion project for RSocket vs gRPC vs WebSocket on Spring Boot 4.1: When Each One Wins on ankurm.com.
One application serves the same two operations over all three protocols. Same JVM, same heap,
same data generator, same five fields — so a difference in the numbers is a difference in the
transports rather than in three separately-written demos.
./scripts/run-all.sh re-measures everything under
docs/output/.
Versions
| Version | Notes | |
|---|---|---|
| JDK | 25 (Temurin 25.0.4.1+1) | current LTS |
| Spring Boot | 4.1.1 | latest GA of the 4.1 line |
| Spring Framework | 7.0.9 | Boot-managed |
| grpc-java | 1.83.1 | Boot-managed. Boot 4.1.0 pinned 1.80.0 — it moved |
| protobuf-java | 4.35.1 | Boot-managed. Boot 4.1.0 pinned 4.34.2 |
| rsocket-java | 1.1.5 | Boot-managed |
| Spring gRPC | 1.1.1 | the programming model; separate version from Boot's |
| Jackson | 3.1.5 (tools.jackson) |
plus jackson-dataformat-cbor, RSocket's default |
Read from repo1.maven.org/.../maven-metadata.xml and Boot's own spring-boot-dependencies POM.
Quickstart
./scripts/run-all.sh # six tests, then re-measure docs/output/
mvn test # the same without the capture
mvn spring-boot:run # gRPC 9090, RSocket 7000, WebSocket ws://localhost:8080/quotes
Nothing to install: protoc and the gRPC codegen plugin resolve as Maven artifacts for your OS
and architecture, and all three servers start inside the test JVM.
The operations
| gRPC | RSocket route | WebSocket command | |
|---|---|---|---|
| one quote | QuoteService/GetQuote |
quote |
QUOTE <symbol> |
| N quotes | QuoteService/StreamQuotes |
quotes |
STREAM <symbol> <n> <delayMs> |
| unbounded | QuoteService/StreamUnbounded |
quotes.unbounded |
UNBOUNDED <symbol> |
same, isReady()-gated |
QuoteService/StreamUnboundedReady |
— | — |
| server-side produced count | QuoteService/Produced |
produced |
PRODUCED |
Documentation
- The same use case, three ways
- How they measure — and why a loopback benchmark flatters the wrong one
- Payload: the one comparison that needs no benchmark
- Back-pressure: the measurement that decides it
- Choosing
Captured output
| File | What it shows |
|---|---|
payload-sizes.txt |
one message as protobuf, JSON and CBOR, in hex |
request-response.txt |
5 000 sequential calls, p50/p99/mean per protocol |
stream-throughput.txt |
50 000 messages on one connection |
backpressure.txt |
how many the server produced for a client that wanted 100 |
grpc-isready.txt |
the documented gRPC fix, tested — and it did not work here |
rsocket-codecs.txt |
what Boot configures for RSocket, and why CBOR wins |
tests.txt |
6 tests |
Read the benchmark honestly
Client and server are the same process on one loopback interface, two cores. There is no network. That deletes protobuf's size advantage, and it flatters whichever protocol does the least work per message — which is why raw WebSocket wins both speed benchmarks and why that is not the recommendation. Treat the ratios as indicative and the absolutes as meaningless off this box.
The one result that does transfer is the back-pressure table, because it is a property of the protocols rather than of the link.
Four things this module exists to prove
- RSocket's
request(n)is real and the others have no equivalent. For a client that took 100 messages then went quiet for two seconds, the servers produced 100, 1 346 233 and 311 617 respectively. ServerCallStreamObserver.isReady()did not bound a gRPC producer here. The documented fix was tested; the transcript is committed as measured, including its instability.- Raw WebSocket is the fastest and has the worst tail. p50 of 124 µs against a p99 of 1 448 µs — a 12x spread, where gRPC's is 3.5x.
- CBOR keeps the field names, and it wins by list order. It is binary JSON, not a schema
format: 67 bytes against protobuf's 35 and JSON's 83. Boot puts
JacksonCborEncoderahead ofJacksonJsonEncoder, and a bareRSocketStrategies.create()has neither.