gRPC vs WebSocket vs JSON-RPC comes down to the job. Use JSON-RPC over HTTP to ask a node a question, WebSocket to be told about new blocks, logs or prices in a browser or from an EVM node, and gRPC to consume a typed, filtered, high-volume stream from a backend. On Dwellir's own Hyperliquid measurements gRPC was not faster than WebSocket: medians were 163 to 190 ms on both.
The reasons to choose gRPC are typed contracts, server-side filters, flow control, replay where the provider offers it, and smaller messages: a Hyperliquid trade is about 28% smaller as protobuf than as JSON. This post explains the differences with measured numbers, shows each transport in one command, and ends with a table of ten common jobs and the transport for each.
What is the difference between gRPC, WebSocket and JSON-RPC?
JSON-RPC is a request and response format, WebSocket is a long-lived two-way connection, and gRPC is a typed RPC framework on HTTP/2 whose calls can stream. They sit at different layers, which is why blockchain APIs mix them: Ethereum's eth_subscribe is JSON-RPC carried over a WebSocket.
- JSON-RPC 2.0 is "a stateless, light-weight remote procedure call (RPC) protocol" (specification). A request names a
method, itsparamsand anid; the response carriesresultorerrorwith the sameid. It has no schema, no subscriptions and no streaming of its own. What is RPC in blockchain covers the basics, and JSON-RPC batch sizing and partial failures covers batches. - WebSocket (RFC 6455) upgrades an HTTP/1.1 connection to a full-duplex channel. Either side can send at any time. It defines framing and ping/pong, but no message format, no schema and no resume. Blockchain nodes send JSON-RPC over it; WebSockets for blockchain developers covers the subscription types.
- gRPC is "a modern RPC protocol implemented on top of HTTP/2" (grpc.io). Services and messages are defined in a
.protofile, clients are generated from it, and messages travel as Protocol Buffers. A call can be unary or stream in either direction.
| JSON-RPC over HTTP | JSON-RPC over WebSocket | gRPC | |
|---|---|---|---|
| Connection | HTTP/1.1 or HTTP/2, one request per call | HTTP/1.1 upgrade, one long-lived socket | HTTP/2, many calls on one connection |
| Encoding | JSON | JSON | Protocol Buffers (binary) |
| Direction | Client asks, server answers | Both ways; server pushes notifications | Unary, server streaming, client streaming, bidirectional |
| Schema | None | None | .proto contract with generated clients |
| Backpressure | Not needed | None in the protocol; Geth closes the socket at 10,000 buffered notifications | HTTP/2 flow control |
| Resume after a drop | Ask for the range again | None; backfill over HTTP | None in gRPC; some servers replay from a cursor |
| Browser | Yes | Yes | Only through gRPC-Web and a proxy |

gRPC vs REST and JSON-RPC
Most "gRPC vs REST" comparisons carry over to JSON-RPC, because both send JSON over HTTP request by request. JSON-RPC is not REST: it uses one endpoint and names the operation in the body, where REST maps operations to resources and HTTP verbs. Against either, gRPC adds a binary encoding, a typed contract, HTTP/2 multiplexing and streaming calls. It costs you tooling: you need the .proto file, generated stubs or a tool such as grpcurl, and proxies that speak HTTP/2 end to end.
How a gRPC stream works
A gRPC server stream is one call that returns many messages. The client sends one request, for example "trades for BTC", and reads messages from the stream until the server ends it. gRPC "guarantees message ordering within an individual RPC call" (core concepts), and every stream ends with a status code in the HTTP/2 trailers, such as UNAVAILABLE or DEADLINE_EXCEEDED. A client always learns why a stream stopped. A dropped WebSocket often reports only a close code, or nothing.
How each transport delivers the same six events
The explainer below sends six Hyperliquid trades to three clients. The polling client asks every 1 or 5 seconds, the WebSocket client is pushed each trade, and the gRPC client reads a stream. The connection drop after trade 3 is on by default; switch it off to compare a clean run. Message sizes are measured; the timing is illustrative.
Six trades, three transports
Trades on the chaint = 10.0 s
Client offline from 3.5 s to 5.0 s, back at 5.5 s
JSON-RPC pollingHTTP
The client asks every 1 s and reads every trade since its last poll.
272 B JSON per trade, plus a 63 B request each poll
- Trade 1: polled, +0.5 s
- Trade 2: polled, +0.5 s
- Trade 3: polled, +0.5 s
- Trade 4: polled, +2.5 s
- Trade 5: polled, +1.5 s
- Trade 6: polled, +0.5 s
- Requests sent
- 10
- Bytes
- 2,397 B
- Average wait
- 1.0 s
- Missing now
- 0
Nothing is lost. The polls during the outage fail, and trades 4 and 5 arrive with the first poll after it.
WebSocket pusheth_subscribe or subscribe
One connection. The server pushes each trade as it happens.
272 B JSON per trade
- Trade 1: pushed
- Trade 2: pushed
- Trade 3: pushed
- Trade 4: HTTP gap-fill, +2.5 s
- Trade 5: HTTP gap-fill, +1.5 s
- Trade 6: pushed
- Requests sent
- 3
- Bytes
- 1,695 B
- Average wait
- 0.7 s
- Missing now
- 0
Trades 4 and 5 are never resent. They arrive only because the client backfills the gap over HTTP after it reconnects.
gRPC streamHTTP/2 server stream
One request opens a stream of typed protobuf messages.
196 B protobuf per trade, about 28% smaller
- Trade 1: pushed
- Trade 2: pushed
- Trade 3: pushed
- Trade 4: Dwellir replay, +2.0 s
- Trade 5: Dwellir replay, +1.0 s
- Trade 6: pushed
- Requests sent
- 2
- Bytes
- 1,176 B
- Average wait
- 0.5 s
- Missing now
- 0
The client reconnects with the cursor of trade 3, and Dwellir replays trades 4 and 5 from a raw-feed cursor, then tails live. gRPC itself has no resume.
Polling never loses a trade, because each poll asks for everything since the last good one, but every trade waits for the next poll. A push over WebSocket is immediate, and anything sent while the client is offline is gone unless the client fetches it again over HTTP. The gRPC lane recovers trades 4 and 5 only because Dwellir's stream accepts a cursor and replays from it. Without a cursor, gRPC loses them as well.
Try each transport in one command
Each example below ran against Dwellir on 3 November 2026. Replace YOUR_API_KEY with a key from the Dwellir dashboard.
JSON-RPC over HTTP: one request, one answer.
curl -s https://api-ethereum-mainnet.n.dwellir.com/YOUR_API_KEY \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_blockNumber","params":[]}'{"jsonrpc":"2.0","id":1,"result":"0x191de9b"}The request body is 63 bytes and the response 45 bytes, before HTTP headers. A client polling every 500 ms sends 172,800 of these a day, and on Ethereum's 12-second blocks 23 of every 24 return a block it has already seen.
JSON-RPC over WebSocket: subscribe once, then the node pushes every new block header. Node.js 22 and later has a global WebSocket.
const ws = new WebSocket(`wss://api-ethereum-mainnet.n.dwellir.com/${process.env.DWELLIR_API_KEY}`);
let seen = 0;
ws.onopen = () =>
ws.send(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_subscribe', params: ['newHeads'] }));
ws.onmessage = ({ data }) => {
const msg = JSON.parse(data);
if (msg.id === 1) return console.log('subscription', msg.result);
const head = msg.params.result;
console.log(Number(head.number), head.hash.slice(0, 18), `${data.length} bytes`);
if (++seen === 3) ws.close();
};subscription 0xf771051ab2545b7fb1a4c3cab85c487f
26336924 0x538a6fd6d9f3d7fa 1838 bytes
26336925 0x309471938b0fced1 1839 bytes
26336926 0xf1da8afc3253abf8 1810 bytesEach newHeads notification is about 1.8 KB of JSON, one every 12 seconds.
gRPC server stream: one call to Dwellir's Hyperliquid MarketStreaming.StreamTrades with the published v3.proto. The API key goes in the x-api-key header, never in the address. The grpcurl guide covers installing grpcurl and its flags.
grpcurl -proto v3.proto \
-H "x-api-key: YOUR_API_KEY" \
-d '{"coins":["BTC"]}' \
api-hyperliquid-mainnet-grpc.n.dwellir.com:443 \
hyperliquid_l1_gateway.v3.MarketStreaming/StreamTrades{
"time": "1793740846014",
"blockNumber": "1208046957",
"trades": [
{
"coin": "BTC",
"side": "B",
"px": "77882.0",
"sz": "0.0007",
"hash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"time": "1793740846014",
"tid": "174021525714712",
"bidUsers": ["0x87a6be3552386cc5266a5596c88022d7d8f4aa97"],
"askUsers": ["0x6ba2ad09aa6629a423b59b71f3564d84ce66c001"]
}
]
}grpcurl prints each protobuf message as JSON for reading. On the wire it is binary. The coin filter runs on the server, so the client receives only BTC trades.
Is gRPC faster than WebSocket?
No, not by itself. In Dwellir's Hyperliquid data latency benchmark, run from AWS Tokyo on 28 October 2026, median latency from block time to client was 163 to 190 ms on both transports. What decides latency is when the provider's node receives the block and how far your client is from it.
| Dwellir feed, AWS Tokyo | WebSocket median | gRPC median |
|---|---|---|
| L2 book, BTC | 177 ms | 170 ms |
| L4 book, BTC | 190 ms | not tested |
| Trades, BTC | 187 ms | not tested |
| Top of book (BBO), BTC | not tested | 174 ms |
| Fills, all coins | not tested | 163 ms |
| Blocks | not tested | 181 ms |
The L2 book is the one feed measured on both: 177 ms over Dwellir's order book WebSocket and 170 ms over gRPC, 7 ms apart in the same run. Gaps between providers on the same transport were larger: on WebSocket trades the public API was 291 ms against 187 ms for Dwellir. QuickNode was 3 to 13 ms ahead of Dwellir at the median on eight of nine gRPC feeds, which again comes down to when each node receives the block, not the transport.
The Hyperliquid mempool stream showed orders a median 357 ms before the public orderUpdates WebSocket. That lead comes from the data source. The mempool feed carries signed actions before a block includes them, and orderUpdates reports them after. Dwellir happens to serve the mempool over gRPC, but the same data over a WebSocket would have the same lead.
HTTP/2 does help in one way: many streams share one connection, and a stalled stream does not block the others at the HTTP level. TCP head-of-line blocking still applies to all of them. High-volume streams also move the bottleneck into your client. The mempool feed runs at 4 to 5 MB a second and needs a compiled or threaded client and a receive limit above gRPC's 4 MiB default.
How big are gRPC messages compared with JSON?
About 28% smaller for a Hyperliquid trade in our test. On 3 November 2026 we read Dwellir's StreamTrades for BTC, ETH, SOL and HYPE for 60 seconds and serialized the same fields as compact JSON, and compared both with the public Hyperliquid WebSocket trades channel over the same minute.
| One Hyperliquid trade | Mean size | Trades |
|---|---|---|
| JSON, public Hyperliquid WebSocket | 272.2 B | 395 |
| JSON, same fields rebuilt from the gRPC trades | 272.3 B | 276 |
| gzip of one JSON trade | about 209 B | 276 and 395 |
Protobuf, Dwellir StreamTrades | 195.8 B | 276 |
Protobuf sizes are the serialized message, without the 5-byte gRPC prefix or HTTP/2 framing. JSON sizes are the trade object, without WebSocket framing or compression.
The saving is modest because a trade is mostly strings: two 42-character addresses, a 66-character hash, and price and size as decimal strings. Protobuf drops the field names and quotes and packs time and tid as varints (encoding guide), but it stores strings as they are. Gzip on a single JSON trade lands close to the protobuf size. Numeric-heavy messages should compress further; we have not measured them, and this result covers one message type over one minute.
An Ethereum newHeads notification is about 1.8 KB of JSON per block. Dwellir's raw Hyperliquid gRPC feeds, such as StreamFills, carry the node's own JSON inside a bytes field, so they get the typed envelope and the cursor but not a smaller payload. The size advantage applies to the 14 typed MarketStreaming streams.

What happens when the connection drops?
None of the three transports resumes on its own. What differs is how the server helps you recover: polling just asks for the range again, eth_subscribe offers nothing, and some gRPC servers replay from a cursor.
Polling keeps its position in your code. If a poll fails, the next one asks for everything since the last block you processed, for example with eth_getLogs over a block range. Nothing is lost; it arrives late.
WebSocket subscriptions die with the connection. Geth's documentation is direct about it: "Notifications are sent for current events and not for past events", and "if the connection is closed all subscriptions that are created over this connection are removed" (Geth pub/sub). Geth also closes the connection of a client that falls 10,000 notifications behind. The standard recovery is to resubscribe, buffer the live events, backfill the gap over HTTP, deduplicate on block hash and log index, then drain the buffer. eth_subscribe reconnects and backpressure has the full pattern and stall detection.
gRPC tells you why the stream ended, and HTTP/2 flow control slows the sender when you read slowly instead of letting a buffer overflow. It does not resume. A new call is a new stream, and the server decides where it starts. Sui's migration cookbook says a subscriber that falls behind is dropped without a graceful close, and tells you to reconnect and backfill from your last processed checkpoint. That is the same job as the WebSocket gap-fill.
Dwellir's Hyperliquid raw streams take a Position (block number or Unix ms timestamp), replay every retained record from it, then tail live data (gRPC docs). This replays the last two minutes of BTC fills and then keeps streaming:
FROM=$(( ($(date +%s) - 120) * 1000 )) # two minutes ago, Unix ms
grpcurl -proto v3.proto -max-msg-sz 16777216 \
-H "x-api-key: YOUR_API_KEY" \
-d "{\"position\":{\"timestamp\":\"$FROM\"},\"filter\":{\"fields\":[{\"field\":\"coins\",\"values\":[\"BTC\"]}]}}" \
api-hyperliquid-mainnet-grpc.n.dwellir.com:443 \
hyperliquid_l1_gateway.v3.HyperliquidL1Gateway/StreamFills \
| jq -c '{blockNumber, timestamp}'{"blockNumber":"1208045766","timestamp":"1793740763158"}
{"blockNumber":"1208045773","timestamp":"1793740763756"}
{"blockNumber":"1208045781","timestamp":"1793740764289"}
...
{"blockNumber":"1208047609","timestamp":"1793740891590"}The first record is from the requested time, and the last is live, about two minutes later. The cursor is inclusive, so deduplicate the last record you committed. A stream that loses records ends with DATA_LOSS (raw feeds) or ABORTED (typed streams), and the fix is to reconnect from the last cursor. Opt-in progress frames keep a quiet filtered stream open through proxies and move the cursor forward. The 14 typed market streams are live only and open with a snapshot where they hold state; replay depth on raw feeds varies by feed. The troubleshooting page lists each status code.
Can browsers use gRPC?
Not directly. A browser cannot make a raw gRPC call, so it needs gRPC-Web and a proxy that translates, by default Envoy. The gRPC-Web README lists unary calls and server streaming, the latter "only when grpcwebtext mode is used", and says client and bidirectional streaming are "not currently supported".
Chains that use gRPC say the same. Cosmos SDK documents its gRPC port 9090 as "not supported in browsers" and offers REST through gRPC-gateway on port 1317 (Cosmos SDK docs). Sui's docs recommend gRPC for backend systems, indexers and typed clients, and GraphQL RPC first for frontends and scripts. For a dashboard, use WebSocket or HTTP from the browser, and keep gRPC on a backend that holds the API key.
Which chains use which?
- Ethereum and EVM chains use JSON-RPC over HTTP for requests and
eth_subscribeover WebSocket fornewHeads,logsandnewPendingTransactions. Nodes offer no gRPC and no replay. - Hyperliquid's public API is JSON over HTTPS plus a WebSocket. Per IP address it allows 10 WebSocket connections, 30 new connections per minute, 1,000 subscriptions and 2,000 messages sent per minute (rate limits, checked 3 November 2026). There are no batch subscriptions; Hyperliquid WebSocket subscription limits works through the arithmetic.
- Dwellir's Hyperliquid gRPC adds 14 typed market streams (book, BBO, trades, fills, candles, mids and more) and raw node feeds with server-side
usersandcoinsfilters and cursor replay. It costs $299 a month on an API plan plus 1 API credit per streamed message, and is included unmetered on Dedicated Node Clusters from $4,000 a month (pricing, product page). Teams that want the public message format keep a WebSocket on Dwellir's order book server. - Sui has moved from JSON-RPC to gRPC and GraphQL. Sui's docs say JSON-RPC "is disabled on Sui Foundation Mainnet full nodes" since the week of 27 July 2026 (Sui gRPC), and its streaming subscriptions with server-side filters replace the deprecated JSON-RPC WebSocket support. Dwellir's Sui gRPC overview and grpcurl setup cover the endpoint.
- Cosmos SDK chains expose the same protobuf services over gRPC (9090) and REST (1317), plus CometBFT RPC on 26657.
- Solana is where gRPC streaming became common, through the Geyser-based Yellowstone interface from Triton One. Dwellir does not offer Solana.
Which should I use?
Use JSON-RPC over HTTP to ask, WebSocket to be told in a browser or by an EVM node, and gRPC to consume a typed, filtered, high-volume feed from a backend. The table maps ten common jobs.
| Job | Use | Why | Watch for |
|---|---|---|---|
Balance reads, eth_call, nonce, gas price | JSON-RPC over HTTP | One question, one answer, every language and browser | Batch many reads; check per-item errors in an HTTP 200 |
| Browser dashboard with live prices or blocks | WebSocket | Native in browsers; gRPC needs gRPC-Web and a proxy | Hyperliquid's 10 connections and 1,000 subscriptions per IP; keep keys server-side |
| Indexer on a chain with gRPC (Sui, Hyperliquid via Dwellir) | gRPC stream plus unary lookups | Typed schema, server-side filters, flow control, cursors | Persist the cursor; Sui drops slow subscribers |
| Indexer on EVM chains | WebSocket logs plus HTTP eth_getLogs backfill | EVM nodes stream over WebSocket only | No replay; reorgs re-send logs with removed: true |
| Hyperliquid trading bot or market-data consumer | gRPC for fills, order statuses and typed books; WebSocket if you use the public format | Latency is similar, so choose by features | Host in Tokyo; orders still go to api.hyperliquid.xyz/exchange |
| Pre-trade signal from pending Hyperliquid orders | gRPC StreamMempoolTxs | The only feed that shows orders before a block | 4 to 5 MB/s, no server filter, raise the 4 MiB receive limit |
| Backfill and historical analysis | JSON-RPC over HTTP or gRPC unary calls | Ranges are request and response by nature | Chunk ranges; node retention limits |
| Pending transactions on an EVM chain | WebSocket newPendingTransactions | Standard subscription type | Contents vary by node |
| Serverless or short-lived jobs | HTTP polling, or a long-lived worker writing to a queue | A 200 ms function cannot hold a stream | Polling cost against a worker |
| Service-to-service inside your stack | gRPC | Contracts, code generation, deadlines, multiplexing | Needs L7 or client-side load balancing |
For Hyperliquid in particular, the Hyperliquid infrastructure stack maps every API surface to the work it does. To stream typed Hyperliquid data with server-side filters and replay, see Dwellir's Hyperliquid gRPC or contact the Dwellir team about a Dedicated Node Cluster.




