How JSON-RPC Powers Every Web3 App You Use

How JSON-RPC Powers Every Web3 App You Use

Every time you check a wallet balance, sign a transaction, or refresh a dApp, you’re using JSON-RPC. Here’s what it actually is, why it matters, and how to use it well.

Open MetaMask. Look at your balance. Behind that number is a JSON-RPC call. Press “send” on a transaction. Another call. Load a DeFi dashboard. Probably a few dozen calls in a single page load.

JSON-RPC is the protocol every Ethereum-compatible blockchain uses to talk to the outside world. If you build on Web3 — even casually — understanding it well saves you hours of debugging and makes the systems you build noticeably faster.

This is the explainer we wish existed when we started.

What JSON-RPC actually is

JSON-RPC is a stateless protocol for asking a remote computer to do something. You send a JSON object describing what you want; the computer sends a JSON object back with the result.

A minimal request:

{
  "jsonrpc": "2.0",
  "method": "eth_blockNumber",
  "params": [],
  "id": 1
}

A minimal response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": "0x14e9c2c"
}

That’s it. Four fields in, three fields out. The method names what you want done. The params array carries arguments. The id lets you match responses to requests when you send several at once.

Every blockchain node — Geth, Erigon, Reth, Bor, Nethermind — exposes the same set of JSON-RPC methods. That’s why a wallet built for Ethereum works on Polygon, Base, Arbitrum, and dozens of other EVM chains without code changes. The protocol is the standard.

The methods that matter

There are dozens of JSON-RPC methods. In practice, most apps use about ten heavily. Knowing what each does — and what it costs — is the difference between a snappy dApp and one that times out.

Reading state

  • eth_blockNumber — what’s the latest block? The cheapest call there is. Use it as a health check.
  • eth_getBalance — how much ETH does an address hold at a specific block? One of the most common calls in any wallet.
  • eth_getCode — is this address a contract or an externally-owned account? Returns the bytecode if it’s a contract.
  • eth_getStorageAt — read raw storage slots of a contract. Niche, but essential for debugging.
  • eth_call — execute a contract function as a read-only call. No transaction, no gas, no state change. The workhorse of every dApp.

Reading history

  • eth_getBlockByNumber / eth_getBlockByHash — fetch a full block, with or without transaction details.
  • eth_getTransactionByHash — look up a specific transaction.
  • eth_getTransactionReceipt — get the result of a transaction after it confirmed: gas used, logs emitted, success or failure.
  • eth_getLogs — search for events emitted by contracts across a block range. This is how indexers and analytics tools work.

Writing state

  • eth_sendRawTransaction — submit a signed transaction to the network. The wallet signs locally; the node just relays it.
  • eth_estimateGas — predict how much gas a transaction will use, before sending.
  • eth_gasPrice / eth_feeHistory — get current network fee conditions.

Subscribing to changes

  • eth_subscribe (WebSocket only) — get pushed updates when new blocks arrive, new transactions hit the mempool, or specific logs are emitted. Drastically more efficient than polling.

Why the same method can be cheap or expensive

This is the part most tutorials skip and most developers learn the hard way.

eth_getLogs looks simple — give it a block range and a filter, get back matching events. But the cost to the node serving it depends entirely on the range:

  • eth_getLogs over the last 100 blocks — milliseconds
  • eth_getLogs over the last million blocks — seconds, possibly a timeout
  • eth_getLogs over the entire chain — likely rejected outright

The same applies to eth_call against historical blocks. Calling a contract at the latest block uses live state. Calling it at a block from a year ago requires the node to reconstruct historical state, which is dramatically more expensive — and only works at all if the node is an archive node.

Practical rule: when you write a query that touches old data, ask yourself whether you’d want to run it. If the answer is “ugh, that sounds slow,” batch it, cache it, or move it to an off-chain indexer.

Archive nodes vs full nodes

Two flavors of nodes exist, and most beginners don’t know the difference until they hit an error.

Full nodes store the chain’s history (blocks, transactions, receipts) but only the current state. Ask for a balance at the latest block — instant. Ask for a balance from a year ago — the node has to replay every transaction since to reconstruct it, which most full nodes simply refuse to do.

Archive nodes store the full historical state. Any balance, any storage slot, any contract call at any block in history — directly accessible. The price is storage: Ethereum’s archive state is multiple terabytes and growing.

If you’re building:
– a wallet showing current balances → full node is fine
– an indexer, analytics tool, or anything that reads historical state → you need archive
– a debugger or transaction tracer → archive plus trace methods (debug_traceTransaction, trace_call)

Archive access is the first thing most paid providers paywall, because it’s the most expensive to provide. rpcfree.com includes it by default across all four supported chains.

Patterns that make dApps fast

A few practices that consistently separate fast Web3 apps from sluggish ones:

1. Batch related calls

Most JSON-RPC providers accept batched requests — multiple calls in a single HTTP round-trip:

[
  {"jsonrpc":"2.0","method":"eth_getBalance","params":["0xabc...","latest"],"id":1},
  {"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":2},
  {"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":3}
]

One request, three results. For dashboards that load five to ten balances at once, batching cuts latency dramatically.

2. Use multicall for read-heavy workloads

The Multicall3 contract (deployed at the same address on dozens of chains) lets you bundle many eth_call invocations into a single on-chain call. One JSON-RPC request, dozens of contract reads.

3. Subscribe instead of poll

If your app needs to react to new blocks or specific events, eth_subscribe over WebSocket is orders of magnitude more efficient than repeatedly calling eth_blockNumber or eth_getLogs. The node pushes updates as they happen.

4. Cache aggressively, invalidate sparingly

Block data is immutable once finalized. A transaction receipt for a transaction from yesterday will never change. Cache freely; the only thing you need to refresh is the chain tip.

5. Handle rate limits like a citizen

Every public RPC has limits. When you receive HTTP 429, respect the Retry-After header. Exponential backoff for repeated failures. Don’t hammer.

A polite client can run all day on a free tier; a rude one gets rate-limited within minutes regardless of how generous the provider is.

Common errors and what they mean

Real errors developers hit, and what they usually indicate:

  • block not found — you’re asking for state at a block that’s beyond the chain tip, or pruned from a full node
  • execution reverted — your eth_call ran a contract that intentionally reverted (often a require failure)
  • gas required exceeds allowance — gas estimation failed, often because the call would revert
  • nonce too low — you’re trying to send a transaction with a nonce already used
  • replacement transaction underpriced — you’re trying to replace a pending transaction without bumping the fee enough
  • out of gas for an eth_call — the call ran but exhausted the gas limit before completing; raise the gas parameter

Reading these correctly is half the work of debugging Web3 apps.

A practical example

Putting it together: how does a wallet display “send 1 ETH to alice.eth” before you press confirm?

  1. eth_blockNumber — what block are we on?
  2. eth_getBalance for the user’s address — do they have enough?
  3. ENS resolution — what address does alice.eth resolve to? (Itself an eth_call to the ENS resolver contract)
  4. eth_estimateGas — how much gas will this cost?
  5. eth_feeHistory or eth_gasPrice — what’s the right fee to attach?
  6. eth_getTransactionCount for the user’s address — what’s the next nonce?

Six calls, before the user has even clicked confirm. After confirm:

  1. eth_sendRawTransaction — submit the signed transaction
  2. Repeated eth_getTransactionReceipt until it appears — track inclusion

The transparency of JSON-RPC is its strength: every step is observable, debuggable, and replaceable. If you understand it, you can build anything that runs on a blockchain.

Where to go from here

  • Browse the Ethereum JSON-RPC specification — the authoritative method list
  • Try methods directly with curl against https://rpcfree.com/ethereum-rpc to build intuition
  • Read your favorite wallet’s source code (most are open) and trace the JSON-RPC calls it makes
  • Build something small that touches each category of method: a balance checker, a transaction tracer, a log indexer

JSON-RPC isn’t magic. It’s a small, well-defined protocol that happens to underlie a trillion-dollar ecosystem. Understanding it deeply pays back constantly.


Try the methods in this article live at rpcfree.com/ethereum-rpc — free, no signup, archive included.