Skip to content
New guideAgent Cookbook 12 copy-ready prompts for AI coding agents.See recipes
All blog posts
gRPC vs WebSocket vs JSON-RPC: Which to Use for Real-Time Blockchain Data

gRPC vs WebSocket vs JSON-RPC: Which to Use for Real-Time Blockchain Data

By Elias Faltin16min read

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, its params and an id; the response carries result or error with the same id. 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 .proto file, clients are generated from it, and messages travel as Protocol Buffers. A call can be unary or stream in either direction.
JSON-RPC over HTTPJSON-RPC over WebSocketgRPC
ConnectionHTTP/1.1 or HTTP/2, one request per callHTTP/1.1 upgrade, one long-lived socketHTTP/2, many calls on one connection
EncodingJSONJSONProtocol Buffers (binary)
DirectionClient asks, server answersBoth ways; server pushes notificationsUnary, server streaming, client streaming, bidirectional
SchemaNoneNone.proto contract with generated clients
BackpressureNot neededNone in the protocol; Geth closes the socket at 10,000 buffered notificationsHTTP/2 flow control
Resume after a dropAsk for the range againNone; backfill over HTTPNone in gRPC; some servers replay from a cursor
BrowserYesYesOnly through gRPC-Web and a proxy
Comparison of JSON-RPC over HTTP, JSON-RPC over WebSocket and gRPC: connection, JSON or protobuf encoding, direction, browser support, resume after a drop (none except cursor replay on Dwellir's raw gRPC streams) and backpressure (Geth closes at 10,000 buffered notifications; gRPC uses HTTP/2 flow control).

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

Poll interval

Trades on the chaint = 10.0 s

1
2
3
4
5
6

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.

Bash
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":[]}'
JSON
{"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.

newheads.mjs
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();
};
Text
subscription 0xf771051ab2545b7fb1a4c3cab85c487f
26336924 0x538a6fd6d9f3d7fa 1838 bytes
26336925 0x309471938b0fced1 1839 bytes
26336926 0xf1da8afc3253abf8 1810 bytes

Each 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.

Bash
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
JSON
{
  "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 TokyoWebSocket mediangRPC median
L2 book, BTC177 ms170 ms
L4 book, BTC190 msnot tested
Trades, BTC187 msnot tested
Top of book (BBO), BTCnot tested174 ms
Fills, all coinsnot tested163 ms
Blocksnot tested181 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 tradeMean sizeTrades
JSON, public Hyperliquid WebSocket272.2 B395
JSON, same fields rebuilt from the gRPC trades272.3 B276
gzip of one JSON tradeabout 209 B276 and 395
Protobuf, Dwellir StreamTrades195.8 B276

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.

Bar chart of one Hyperliquid trade in three encodings: 272 bytes as JSON on the public WebSocket, about 209 bytes gzipped, and 195.8 bytes as protobuf on Dwellir StreamTrades, about 28% smaller than JSON, measured over 60 seconds on 3 November 2026.

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:

Bash
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}'
Text
{"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_subscribe over WebSocket for newHeads, logs and newPendingTransactions. 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 users and coins filters 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.

JobUseWhyWatch for
Balance reads, eth_call, nonce, gas priceJSON-RPC over HTTPOne question, one answer, every language and browserBatch many reads; check per-item errors in an HTTP 200
Browser dashboard with live prices or blocksWebSocketNative in browsers; gRPC needs gRPC-Web and a proxyHyperliquid'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 lookupsTyped schema, server-side filters, flow control, cursorsPersist the cursor; Sui drops slow subscribers
Indexer on EVM chainsWebSocket logs plus HTTP eth_getLogs backfillEVM nodes stream over WebSocket onlyNo replay; reorgs re-send logs with removed: true
Hyperliquid trading bot or market-data consumergRPC for fills, order statuses and typed books; WebSocket if you use the public formatLatency is similar, so choose by featuresHost in Tokyo; orders still go to api.hyperliquid.xyz/exchange
Pre-trade signal from pending Hyperliquid ordersgRPC StreamMempoolTxsThe only feed that shows orders before a block4 to 5 MB/s, no server filter, raise the 4 MiB receive limit
Backfill and historical analysisJSON-RPC over HTTP or gRPC unary callsRanges are request and response by natureChunk ranges; node retention limits
Pending transactions on an EVM chainWebSocket newPendingTransactionsStandard subscription typeContents vary by node
Serverless or short-lived jobsHTTP polling, or a long-lived worker writing to a queueA 200 ms function cannot hold a streamPolling cost against a worker
Service-to-service inside your stackgRPCContracts, code generation, deadlines, multiplexingNeeds 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.

Start free.1 response = 1 credit.

100,000 responses a day at 20 per second, on 150+ networks. Upgrade when you need more.