Monad RPC Limits: eth_getLogs, Gas Caps, WebSocket

Monad RPC limits measured on 24 October 2026: eth_getLogs spans per public host, the 150M gas cap, batch limits, finality and monadNewHeads commit states.

Monad RPC Limits: eth_getLogs, Gas Caps, WebSocket

On Monad’s public endpoints, eth_getLogs accepts at most 101 blocks per request (toBlock minus fromBlock of at most 100) on rpc.monad.xyz and the Monad Foundation endpoints, while other public providers allow more: 1,000 blocks on rpc3.monad.xyz and 20,000 on rpc2.monad.xyz with an address filter. eth_call and eth_estimateGas are capped at 150,000,000 gas on every endpoint we measured, and batches are limited or refused outright. We measured all of this on 24 October 2026, between 12:05 and 12:40 UTC.

This article lists the numbers per host, shows what the errors look like, gives you a tested eth_getLogs pager and covers block time, finality and Monad’s own WebSocket subscriptions. Where Monad’s documentation and our measurements disagree, both values are shown. For the endpoint URL, chain ID and wallet setup, see the Monad network page.

Public endpoints change without notice, so treat every number below as a dated snapshot and re-measure before you build on it.

The public endpoints we measured

Monad’s Network Information page lists five mainnet endpoints. We use the short names below.

  • rpc: rpc.monad.xyz (QuickNode per the docs)
  • rpc1: rpc1.monad.xyz (Alchemy per the docs)
  • rpc2: rpc2.monad.xyz (Goldsky Edge)
  • rpc3: rpc3.monad.xyz (Ankr)
  • mf: rpc-mainnet.monadinfra.com (Monad Foundation)

One oddity: rpc1 is documented as an Alchemy endpoint, but in our tests it behaved like the Foundation edge. Its response headers matched mf, it rejected batches the same way and it capped eth_getLogs at the same span. Name the hosts, not the providers, in your own notes.

Measured limits per public endpoint

Endpointeth_getLogs blocks per requesteth_call and eth_estimateGas gasBatchState depth
rpc101150,000,00050 items per secondabout 8 days
rpc1101150,000,000any batch refusedfull history
rpc220,000 with an address filternot measured150 items passedfull history
rpc31,000 filtered, 727 unfiltered150,000,00010 itemsabout 32 hours
mf101150,000,000any batch refusedfull history

How to read it:

  • Blocks per request are counted inclusively, so 101 blocks is a span (toBlock minus fromBlock) of 100. On rpc, rpc1, mf and the testnet endpoint, 101 blocks counted inclusively pass and 102 fail. On rpc the same result held with a topic filter, an address filter and no filter at all. On rpc an unfiltered 101-block call returned 8,965 logs, so we saw no log-count cap there.
  • Gas is the largest gas value that succeeded. 150,000,000 passed and 150,000,001 failed on all four hosts. That equals the block gas limit, which was 150,000,000 in all 1,001 blocks we sampled.
  • Batch on rpc is a per-second item quota: a batch of 50 passed, and in a batch of 51 only 50 items succeeded. rpc1 and mf returned the plain text Restricted JSON RPC method for any array, even one item, while single requests worked.
  • State depth is how far back eth_getBalance worked, found by binary search: 2,316,645 blocks on rpc, 384,991 blocks on rpc3, and on rpc1, rpc2 and mf state read back identically at blocks 30,000,000, 60,000,000 and 100,000,000, which we take as full history.

The single-request rate limit is not in the table because we could not measure it cleanly without hammering shared infrastructure. Eighty parallel eth_chainId calls on rpc all succeeded, but our own client throttled itself. On rpc3, the 16th consecutive 10-item batch drew HTTP 429 with code -32090 and the message Too many requests, reason: call rate limit exhausted, retry in 10s, while a single 11-item batch is refused at once with 413 and code -32062. For the general mechanics of limits and backoff, read RPC rate limits explained.

Where Monad’s docs and our measurements disagree

Monad documents its limits on the JSON-RPC overview and the Network Information page. Neither page shows a date on screen; their page metadata gives last-modified dates of 22 October and 4 September 2026. Here is each mismatch, with the docs value first and our measurement second.

  • Gas cap for eth_call. Docs: 200M gas on rpc, rpc1 and mf, 1B on rpc3. Measured on 24 October 2026: 150,000,000 on all four. Above 200,000,000 the message differs (gas limit too high on rpc, user-specified gas exceeds provider limit on the others), but the real ceiling is the block gas limit.
  • Request rate and batches on rpc. Docs: 25 requests per second, batch of 100. Measured: 50 batch items per second, with the 51st item in a batch failing. We did not measure the docs’ 25 rps for single calls.
  • Batches on rpc2 and mf. Docs: rpc2 batch of 10, mf batch of 1. Measured: a 150-item batch fully succeeded on rpc2, and mf refused every array.
  • eth_getLogs on rpc1. Docs: 1,000 blocks and 10,000 logs, whichever is more constraining. Measured: the same 101-block limit (span 100) as rpc.
  • Block time. One docs page says “2.5 blocks every second” (400 ms), while others say 300 ms. We measured 302 ms. More in the next section.
  • debug_traceCall. Docs: it returns -32602 Invalid params if you omit the trace options. Measured: it worked without options on rpc and rpc1 and returned a call-tracer result, although the docs also say debug_ methods are disabled on rpc1. On rpc3 the method is disabled.
  • newPendingTransactions. Docs: not supported. Measured: see the WebSocket section.
  • State depth. Docs: roughly 40,000 blocks on a 2 TB SSD. Measured: about 2.3 million blocks on rpc, about 385,000 on rpc3 and full history on rpc1, rpc2 and mf.

Do not plan around the optimistic number. When docs and a live call disagree, the live call is what your code will hit.

What the errors look like and how to page eth_getLogs

Error codes for the same problem differ by host, so do not key your handling on the code alone. Here is the range error per endpoint:

  • rpc and the testnet endpoint: -32614, eth_getLogs is limited to a 100 range
  • rpc1 and mf: -32602, block range too large
  • rpc3: -32062, Block range is too large, and for a busy unfiltered range -32000, response exceeds size limit
  • rpc2: -32012, getLogs request exceeded max allowed range

-32602 is also the generic “Invalid params” code, so a range error on rpc1 looks like a malformed request unless you read the message. Other errors you will meet:

  • -32007, 50/second request limit reached: an item in an over-quota batch on rpc
  • -32062, Batch size too large with HTTP 413: more than 10 items on rpc3
  • -32000, gas limit too high: gas above the cap on rpc

The reliable fix for a range error is to page the range. The script below walks a block range in windows of 100 blocks, runs four windows at a time and merges the results in order. It uses viem, which ships a Monad chain definition. Swap RPC_URL for your own endpoint and set MAX_BLOCKS to whatever your provider allows.

import { createPublicClient, http, parseAbiItem } from 'viem';
import { monad } from 'viem/chains';

const RPC_URL = process.env.RPC_URL ?? 'https://rpc.monad.xyz'; // swap for your own endpoint
const MAX_BLOCKS = Number(process.env.MAX_BLOCKS ?? 100);     // blocks per eth_getLogs call
const WMON = '0x3bd359C1119dA7Da1D913D1C4D2B7c461115433A';
const transfer = parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)');

const client = createPublicClient({ chain: monad, transport: http(RPC_URL, { retryCount: 3, retryDelay: 500 }) });

// Walk [from, to] in windows of MAX_BLOCKS blocks (inclusive), a few at a time.
async function getLogsChunked({ from, to, concurrency = 4, ...filter }) {
  const windows = [];
  for (let start = from; start <= to; start += BigInt(MAX_BLOCKS)) {
    const end = start + BigInt(MAX_BLOCKS) - 1n;
    windows.push([start, end < to ? end : to]);
  }
  const out = new Array(windows.length);
  let next = 0;
  await Promise.all(Array.from({ length: concurrency }, async () => {
    while (next < windows.length) {
      const i = next++;
      const [fromBlock, toBlock] = windows[i];
      out[i] = await client.getLogs({ ...filter, fromBlock, toBlock });
    }
  }));
  return { logs: out.flat(), calls: windows.length };
}

const chainId = await client.getChainId();
const head = await client.getBlockNumber();
const to = head - 2n;                 // stay behind the tip
const from = to - 999n;               // 1,000 blocks = about 5 minutes on Monad
const t0 = Date.now();
const { logs, calls } = await getLogsChunked({ from, to, address: WMON, event: transfer });
console.log({ chainId, head, from, to, calls, logs: logs.length, ms: Date.now() - t0 });

// What the same 1,000-block request does without chunking:
try {
  await client.getLogs({ address: WMON, event: transfer, fromBlock: from, toBlock: to });
} catch (e) {
  console.log('single 1,000-block call ->', e.details ?? e.shortMessage);
}

We ran it with viem 2.57.11 on Node v24.19.0 at 12:34 UTC against https://rpc.monad.xyz:

{ chainId: 143, head: 116184782n, from: 116183781n, to: 116184780n, calls: 10, logs: 161, ms: 499 }
single 1,000-block call -> eth_getLogs is limited to a 100 range

Ten calls of 100 blocks returned 161 WMON Transfer logs, and the single 1,000-block call failed with the range error. Running the same script with only a URL change and MAX_BLOCKS=1000 against rpc3.monad.xyz used one call and also returned 161 logs for its own window.

To check that paging loses nothing, we fetched one fixed window of 1,000 blocks twice: as ten chunks of 100 on rpc and as a single call on rpc3. Both returned 161 logs, and comparing them by transaction hash and log index showed them identical.

Monad produces about 3.3 blocks per second, so a day is roughly 288,000 blocks. At 100 blocks per call that is about 2,880 calls per day per filter. The script ends its range two blocks behind the head, which is about where the finalized tag sits (see the finality section below). Reorg handling, deduplication and storage are covered in the production eth_getLogs pipeline guide.

Block time and finality

Monad’s docs give a 300 ms block time, 600 ms finality (2 slots) and 300 ms speculative finality (1 slot), on the current facts page. Our measurements agree with 300 ms:

  • 302 ms mean over 1,000 consecutive blocks on rpc (302 seconds of timestamps)
  • 305 ms mean over 200 blocks
  • 302 ms mean arrival gap on a live newHeads stream, 98 headers in 30 seconds with no gaps
  • 300 ms on the testnet over 200 blocks

Block timestamps are whole seconds, so a single gap between two blocks is 0 or 1 second. Only averages over many blocks mean anything. The one docs page that says “2.5 blocks every second” is the real-time data sources page, which contradicts the 300 ms stated elsewhere on the same site. Use 300 ms.

Finality shows up in the block tags. Monad maps latest to the Proposed state, safe to Voted and finalized to Finalized, and pending behaves the same as latest. In 12 of 12 samples on rpc, latest minus safe was 1 block and latest minus finalized was 2 blocks. Two blocks at about 300 ms each is the documented 600 ms.

Which tag to read depends on the job:

  • latest is fastest but backed by speculative execution, so the data may never become final
  • safe suits a UI that wants a stronger signal with little delay
  • finalized suits anything that moves money or writes to a database you cannot roll back

Real-time data over WebSocket

Monad extends the standard Ethereum subscriptions. We tested wss:// on rpc, rpc1, rpc3, mf and the testnet endpoint, and all four subscription types below returned a subscription id on each of them:

  • newHeads: a header when a block is Proposed and speculatively executed
  • logs: matching logs at the same point
  • monadNewHeads: the same headers, plus blockId and commitState, once per state change
  • monadLogs: the same for logs

wss://rpc2.monad.xyz opened and then closed with code 1006 within about 0.3 seconds, and the docs list no WebSocket URL for it. syncing and an invented type both returned -32602 Invalid params while the valid types worked on the same socket, so the error is a real answer and not a dead connection.

The four commit states

monadNewHeads sends up to four messages per block, one for each commit state: Proposed, Voted, Finalized and Verified. We measured 65 of each in a 20-second run. Seen from our test client, a block took on average:

  • 293 ms from Proposed to Voted
  • 555 ms from Proposed to Finalized (minimum 387 ms, maximum 712 ms)
  • 1,463 ms from Proposed to Verified

That is 124 full lifecycles out of 134 blocks seen in a 40-second run on rpc. The figures include network delay from our test machine to the endpoint, so your numbers will differ. Monad’s docs say Voted may be skipped when consensus runs ahead of execution; we saw it skipped 0 times in those 124. They also say an abandoned block gets no explicit event and the docs call a competing proposal for the same height very rare. We saw none in 134 blocks. If you act on a Proposed block, you must handle the case that it never finalizes.

Here is the sample we tested. It subscribes to both newHeads and monadNewHeads and groups the states by blockId. Install the one dependency first with npm i ws.

import WebSocket from 'ws';

const WS_URL = process.env.WS_URL ?? 'wss://rpc.monad.xyz'; // swap for your own endpoint
const RUN_SECONDS = Number(process.env.RUN_SECONDS ?? 10);

const ws = new WebSocket(WS_URL);
const subs = new Map(); // subscription id -> name
const lifecycle = new Map(); // blockId -> { number, states: [commitState...] }
let heads = 0;

ws.on('open', () => {
  ws.send(JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_subscribe', params: ['newHeads'] }));
  ws.send(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'eth_subscribe', params: ['monadNewHeads'] }));
});

ws.on('message', (raw) => {
  const msg = JSON.parse(raw.toString());
  if (msg.id) { // subscribe replies
    if (msg.error) console.error('subscribe failed:', msg.id, msg.error);
    else subs.set(msg.result, msg.id === 1 ? 'newHeads' : 'monadNewHeads');
    return;
  }
  const { subscription, result } = msg.params;
  if (subs.get(subscription) === 'newHeads') {
    heads++;
  } else {
    const entry = lifecycle.get(result.blockId) ?? { number: parseInt(result.number, 16), states: [] };
    entry.states.push(result.commitState);
    lifecycle.set(result.blockId, entry);
  }
});

setTimeout(() => {
  ws.close();
  console.log(`newHeads: ${heads} headers in ${RUN_SECONDS}s`);
  const done = [...lifecycle.values()].filter((b) => b.states.includes('Finalized'));
  console.log(`monadNewHeads: ${lifecycle.size} blocks seen, ${done.length} reached Finalized`);
  for (const b of done.slice(0, 3)) console.log(' block', b.number, b.states.join(' -> '));
}, RUN_SECONDS * 1000);

Output from wss://rpc.monad.xyz at 12:35 UTC:

newHeads: 31 headers in 10s
monadNewHeads: 36 blocks seen, 32 reached Finalized
 block 116184893 Proposed -> Voted -> Finalized -> Verified
 block 116184892 Finalized -> Verified
 block 116184894 Proposed -> Voted -> Finalized -> Verified

Block 116184892 shows only two states because the subscription joined mid-lifecycle. A Finalized message for a block arrives about when the next block is Proposed, and a Verified message refers to a block four heights behind the newest Proposed one. We did not test reconnect logic or ping handling, so add your own before production, as covered in WebSocket vs polling, measured.

Do not subscribe to every log

An unfiltered logs subscription delivered roughly 200 to 230 messages per second on mainnet in our runs, and an unfiltered monadLogs subscription roughly 770 to 900 per second. The rate follows chain activity, so treat these as orders of magnitude. A logs subscription filtered to one address (WMON) delivered 2 logs in 15 seconds while newHeads on the same socket kept up with every block.

Always filter by address and topic. On BoltRPC every pushed WebSocket message bills once, so an unfiltered log stream at hundreds of messages per second is hundreds of billed pushes per second.

newPendingTransactions

Monad’s docs say syncing and newPendingTransactions are not supported, and there is no global mempool. On rpc1, rpc3 and mf the subscribe call returned -32602 Invalid params, as documented. On rpc and the testnet endpoint it did something stranger: the subscribe call returned a subscription id, then the server closed the socket after 1.8 to 3.3 seconds with code 1001 and the reason upstream went away. No pending transaction ever arrived. A control socket subscribed to newHeads only stayed open for 30 seconds and delivered 97 headers. Do not build on pending transactions on Monad.

Methods that are not there

Some methods that exist on other EVM chains do not exist here. Each result below was checked against a control call (eth_chainId) that succeeded on the same host:

  • eth_getProof returns -32601 Method not found on rpc, rpc1 and rpc3
  • eth_newFilter, eth_newBlockFilter and eth_getFilterChanges return -32601 on rpc and rpc1, and -32075 Method disabled on rpc3
  • trace_block returns -32601 on rpc and rpc1
  • txpool_content and eth_blobBaseFee return -32601 (on rpc3, txpool_content returns -32075)

eth_getBlockReceipts works on rpc. Since filter methods are missing, poll eth_getLogs in windows or use a filtered logs subscription instead of installing a filter.

History depth differs by host

Monad’s docs say full nodes do not serve arbitrary old state, only recent state. How recent is a host decision. Reading a balance at an old block worked back about 8 days on rpc, about 32 hours on rpc3 and full history on rpc1, rpc2 and mf. The failure message is Block requested not found. Request might be querying historical state that is not available. If possible, reformulate query to point to more recent blocks, with code -32602. Block lookups are a different matter: eth_getBlockByNumber a million blocks back worked on all three hosts. Treat these depths as a snapshot from 12:32 UTC on 24 October 2026.

Running this on BoltRPC

BoltRPC has Monad (chain ID 143) live with a native WebSocket, so subscriptions are served by the node and not synthesised. Debug methods are available and trace methods are not. We repeated the key measurements through a BoltRPC key on 24 October 2026:

  • eth_getLogs on WMON Transfer events returned 151 logs for 1,000 blocks in 0.6 seconds and 3,615 logs for 20,000 blocks in 2.2 seconds. The 100-block limit of the Foundation endpoints does not apply.
  • eth_call with gas set to 150,000,001 is refused with user-specified gas exceeds provider limit, the same 150,000,000 cap as every public endpoint.
  • monadNewHeads works over the WebSocket endpoint: a 4-second run delivered Proposed, Voted and Finalized updates.
  • eth_getProof returns -32601 Method not found, as on rpc.monad.xyz.

The general docs describe eth_getLogs on the gateway like this: no hard block-range cap, a wide range is split into chunks and fanned out across the chain’s nodes server-side, then merged into one answer. A single request returns at most 250,000 logs; past that it is refused with an error that names a narrower range that fits. Each request has a 60-second deadline. In our own test on Ethereum the same day, the cap that refused a busy query was stated in bytes (query returned more than 268435456 bytes) rather than logs, and the error still named a narrower block range. Test your widest Monad filter on your own key.

The URLs follow the usual pattern:

https://endpoints.boltrpc.io/rpc/YOUR_API_KEY/monad
wss://endpoints.boltrpc.io/ws/YOUR_API_KEY/monad

Both scripts above read the endpoint from RPC_URL and WS_URL, so you can point them at BoltRPC without editing the code. Start with MAX_BLOCKS at 1,000 and raise it while the call still succeeds. We did not test monadLogs through BoltRPC; monadNewHeads worked as described above. The BoltRPC docs do not state whether Monad has archive access, so ask before you plan around deep history. Request unit costs are on the pricing page.

FAQ

What is the eth_getLogs block range limit on Monad?

On the public endpoints we measured on 24 October 2026, rpc.monad.xyz, rpc1.monad.xyz and rpc-mainnet.monadinfra.com accept at most 101 blocks per request (a span of 100), rpc3.monad.xyz accepts 1,000 blocks with a filter and rpc2.monad.xyz accepted 20,000 blocks with an address filter. Page larger ranges in windows of 100 blocks.

What is the maximum gas for eth_call on Monad?

150,000,000 gas on the four public endpoints we measured (rpc, rpc1, rpc3 and mf), for both eth_call and eth_estimateGas. That equals the block gas limit. Monad’s docs list 200M or 1B for some of these endpoints, but 150,000,001 failed everywhere we tried it.

What is the Monad RPC rate limit?

It depends on the host. Monad’s docs list 25 requests per second on rpc.monad.xyz, 15 on rpc1.monad.xyz, 20 on the Foundation endpoint and 300 per 10 seconds on rpc2 and rpc3. We measured a limit of 50 batch items per second on rpc, a 10-item batch limit on rpc3 and no batches at all on rpc1 and the Foundation endpoint. We did not measure single-call rates.

Does Monad support newPendingTransactions?

No. Monad’s docs list it as unsupported, and rpc1, rpc3 and the Foundation endpoint return -32602. On rpc.monad.xyz the subscribe call returns an id, then the server closes the socket with code 1001 after 2 to 3 seconds and sends nothing.

What is monadNewHeads?

It is a Monad-specific WebSocket subscription that sends each block header once per commit state (Proposed, Voted, Finalized and Verified) with a blockId and a commitState field. On rpc.monad.xyz a block went from Proposed to Finalized in 555 ms on average as seen from our client.

Test your Monad limits on your own key

Run the two scripts against your BoltRPC endpoint and see which filters and subscriptions pass. The free trial lasts 14 days.

Start your free 14-day trial: dashboard.boltrpc.io/signup

Questions we get about this.

Content is for informational purposes only and does not constitute financial, legal or technical advice. Code examples and configurations are provided as-is. Verify against official documentation and test in your own environment before deploying to production.

More from the blog.

All articles →
Pricing

Alchemy Alternative: What Switching Really Costs

Switching from Alchemy pays off for standard JSON-RPC at volume. A worked 50M-call bill, what you give up and the migration steps.

Oct 24, 2026 · 18 min read
RPC

eth_getLogs Block Range Limits by Chain, Measured

No standard eth_getLogs block range limit exists. We measured 15 chains on 24 October 2026, from 10 blocks to no cap, and what each -32005 error means.

Oct 24, 2026 · 32 min read
RPC

Migrating from Grove Portal: A Practical RPC Move

Grove has wound down and its rpc.grove.city endpoints no longer resolve. Map your endpoints, check chain coverage and move a production app without a rewrite.

Oct 24, 2026 · 7 min read