You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug: public RPC rate-limits (error -32011 / "request limit reached") break waitForTransactionReceipt, frontend approve calls, and keeper batch jobs — no official fallback documented #207
The Arc Testnet public RPC (rpc.testnet.arc.network) enforces a per-connection request rate limit that returns JSON-RPC error code -32011 ("request limit reached") when exceeded. This error surfaces in at least four distinct ways depending on which layer hits it, and each way has a different failure mode:
Layer
How it appears
Failure mode
Frontend / MetaMask eth_sendRawTransaction
ContractFunctionRevertedError with reason "Request is being rate limited"
User sees a false contract-revert toast — looks like their approve/write failed on-chain
viem waitForTransactionReceipt
RpcRequestError wrapping -32011, hits the else → emit.reject branch immediately
Receipt wait aborts on the first throttled poll even if the tx already confirmed — discussed in detail in #203
JSON-RPC batch requests
Individual items in a batch can return -32011 while others succeed — not a full circuit breaker
Partial batch failures are silently swallowed by callers that don't inspect per-item errors
Server-side viem clients (keeper, relay)
RpcRequestError propagated up through readContract / writeContract
Keeper tick fails, relay returns HTTP 500, session open fails
Error codes / fingerprints
JSON-RPC: { "code": -32011, "message": "request limit reached" }
Custom selector (in revert data): 0x4cef52
viem shortMessage: "RPC Request failed."
MetaMask toast: "The contract function '...' reverted with the following reason: Request is being rate limited."
Root cause
All Arc Testnet tooling defaults to a single RPC URL with no fallback and no retry on non-not-found RPC errors:
viem's waitForTransactionReceipt catch block treats RpcRequestError as terminal — retryCount: 6 only guards the replacement-detection path, not the main receipt fetch
Arc sample repos (arc-escrow, arc-fintech, etc.) all instantiate createPublicClient with a single hardcoded URL
No official list of alternative RPC providers is documented anywhere in this repo
Under even moderate load (multiple concurrent DApp users, a keeper batch job, or a metered nanopayment session), the single public endpoint is enough to trigger -32011.
Reproduction
// viem — a single rate-limited poll terminates the entire waitconstclient=createPublicClient({transport: http("https://rpc.testnet.arc.network")});awaitclient.waitForTransactionReceipt({hash: tx});// If ANY single poll hits -32011 → RpcRequestError → immediate reject// even if the tx is already confirmed on-chain
For the MetaMask / frontend path:
1. Connect MetaMask to Arc Testnet (Chain ID 5042002)
2. Call approve() on USDC (0x3600...0000) while another tab is also polling the same RPC
3. MetaMask shows: "The contract function 'approve' reverted with the following reason: Request is being rate limited."
4. The transaction may or may not have actually failed — the error is from the RPC, not the EVM
Workarounds (tested on Arc Testnet, Chain ID 5042002)
1. viem fallback() transport — distributes load across providers
viem's fallback transport retries each RPC call against the next provider on transport-level errors, reducing how often -32011 fires. This is a mitigation, not a fix — a single -32011 on a sequential eth_getTransactionReceipt poll still terminates waitForTransactionReceipt regardless of which provider is behind the transport, because the rejection happens above the transport layer.
2. SDK-level retry for waitForTransactionReceipt
The load-bearing fix. The SDK (or DApp) must catch RpcRequestError / error code -32011 and retry the receipt lookup before treating it as a definitive failure:
import{RpcRequestError}from"viem";asyncfunctionwaitForReceiptWithRetry(client: PublicClient,hash: `0x${string}`,maxAttempts=5,): Promise<TransactionReceipt>{for(leti=0;i<maxAttempts;i++){try{returnawaitclient.waitForTransactionReceipt({ hash,pollingInterval: 2000});}catch(err){constisRateLimit=errinstanceofRpcRequestError&&(errasany).code===-32011;if(isRateLimit&&i<maxAttempts-1){awaitnewPromise(r=>setTimeout(r,1000*(i+1)));// exponential back-offcontinue;}throwerr;// real error or exhausted retries}}thrownewError("exhausted retries");}
3. User-facing error normalisation
-32011 surfacing as a false contract-revert is confusing. A short check before showing errors:
functionformatTxError(err: unknown): string{constraw=(errasany)?.shortMessage??(errasany)?.message??String(err);if(/rate.?limit|requestlimitreached/i.test(raw)||raw.includes("0x4cef52")){return"Arc Testnet RPC is rate-limiting requests — please wait a few seconds and try again.";}if(/userrejected/i.test(raw))return"Transaction cancelled.";returnraw;}
4. MetaMask / wallet — pass all RPC URLs in wallet_addEthereumChain
MetaMask uses the first URL and falls back to subsequent ones on failure.
Asks
Document the alternative RPC endpoints (drpc, quicknode, blockdaemon) officially in this repo — ideally in the Arc Testnet reference table and in sample repo READMEs. Currently they're undocumented and builders have no way to know they exist.
Raise the public RPC rate limit or provide a rate-limit-free builder endpoint — even a simple API-key scheme would be a significant improvement for teams running keeper jobs or load tests.
Summary
The Arc Testnet public RPC (
rpc.testnet.arc.network) enforces a per-connection request rate limit that returns JSON-RPC error code -32011 ("request limit reached") when exceeded. This error surfaces in at least four distinct ways depending on which layer hits it, and each way has a different failure mode:eth_sendRawTransactionContractFunctionRevertedErrorwith reason"Request is being rate limited"waitForTransactionReceiptRpcRequestErrorwrapping -32011, hits theelse → emit.rejectbranch immediatelyRpcRequestErrorpropagated up throughreadContract/writeContractError codes / fingerprints
Root cause
All Arc Testnet tooling defaults to a single RPC URL with no fallback and no retry on non-not-found RPC errors:
@circle-fin/x402-batchingGatewayClientbuilds its transport ashttp(rpcUrl)with no fallback and no batch (as established in bug: GatewayClient mislabels receipt-wait failures as "Mint transaction failed" and never checks receipt.status for real reverts #203)waitForTransactionReceiptcatch block treatsRpcRequestErroras terminal —retryCount: 6only guards the replacement-detection path, not the main receipt fetcharc-escrow,arc-fintech, etc.) all instantiatecreatePublicClientwith a single hardcoded URLUnder even moderate load (multiple concurrent DApp users, a keeper batch job, or a metered nanopayment session), the single public endpoint is enough to trigger -32011.
Reproduction
For the MetaMask / frontend path:
Workarounds (tested on Arc Testnet, Chain ID 5042002)
1. viem
fallback()transport — distributes load across providersviem's
fallbacktransport retries each RPC call against the next provider on transport-level errors, reducing how often -32011 fires. This is a mitigation, not a fix — a single -32011 on a sequentialeth_getTransactionReceiptpoll still terminateswaitForTransactionReceiptregardless of which provider is behind the transport, because the rejection happens above the transport layer.2. SDK-level retry for
waitForTransactionReceiptThe load-bearing fix. The SDK (or DApp) must catch
RpcRequestError/ error code -32011 and retry the receipt lookup before treating it as a definitive failure:3. User-facing error normalisation
-32011 surfacing as a false contract-revert is confusing. A short check before showing errors:
4. MetaMask / wallet — pass all RPC URLs in
wallet_addEthereumChainMetaMask uses the first URL and falls back to subsequent ones on failure.
Asks
Document the alternative RPC endpoints (drpc, quicknode, blockdaemon) officially in this repo — ideally in the Arc Testnet reference table and in sample repo READMEs. Currently they're undocumented and builders have no way to know they exist.
Fix
waitForTransactionReceiptin@circle-fin/x402-batching— wrap the receipt fetch in SDK-level retry that distinguishes -32011 from definitive on-chain results. This is the same root cause as bug: GatewayClient mislabels receipt-wait failures as "Mint transaction failed" and never checks receipt.status for real reverts #203, just with a more specific error path.Raise the public RPC rate limit or provide a rate-limit-free builder endpoint — even a simple API-key scheme would be a significant improvement for teams running keeper jobs or load tests.
Emit a warning in the
BatchSizeLimitMiddlewarepath (see test(rpc): cover BatchSizeLimitMiddleware with max_entries=0 #154) when the request count approaches the rate limit, so operators can tune--rpc.batch-request-limitbefore they start seeing -32011.Related
GatewayClientfalse-failure on receipt-wait; -32011 is the proximate cause of the intermittent rejectionsBatchSizeLimitMiddleware— batch-level enforcement, adjacent concernarc-fintech#30— extendswithRetryfor transient network errors; -32011 is the JSON-RPC equivalent of the HTTP 429 already handled there