The unhandled JSON-RPC error -32007: Request limit exceeded in Viem occurs when querying QuickNode RPC endpoints under high concurrency. Viem’s default shouldRetry transport handler only checks for standard HTTP 429 status codes and standard RPC -32005 errors, treating -32007 as a fatal non-retryable exception. Configuring a custom retryCount and intercepting error code -32007 in http() transport options restores automated backoff.
To inspect transaction payloads and gas parameters when diagnosing RPC failures, use our ERC-4337 Calldata Decoder Tool. For related framework errors, check our guide on Viem EIP-712 nested struct hash mismatch fixes.
The Technical Anatomy of the -32007 Bypass
To understand why Viem drops the ball on QuickNode rate limits, we must examine the internal request handler in Viem’s source code (src/utils/buildRequest.ts).
Viem evaluates whether a failed request is eligible for retry via an internal predicate named shouldRetry:
// Viem's internal default retry predicate (simplified)
export function shouldRetry(error: Error): boolean {
if ('code' in error) {
const code = (error as { code: number }).code
if (
code === -1 ||
code === -32005 || // LimitExceededRpcError
code === -32603 // InternalRpcError
) return true
}
if (error instanceof HttpRequestError) {
const status = error.status
if (
status === 408 || // Request Timeout
status === 413 || // Payload Too Large
status === 429 || // Too Many Requests
status === 500 || // Internal Server Error
status === 502 || // Bad Gateway
status === 503 || // Service Unavailable
status === 504 // Gateway Timeout
) return true
}
return false
}
Notice the mismatch:
- HTTP Status 200 OK: QuickNode’s proxy returns standard HTTP
200 OKstatus headers for rate-limited queries, returning the error inside the JSON-RPC response payload. Hence, thestatus === 429check insideHttpRequestErroris never triggered. - Proprietary RPC Code: The JSON-RPC specification (RFC-7693) standardizes
-32005as “Limit exceeded”. However, QuickNode utilizes-32007specifically to distinguish credit/rate threshold boundaries ("N/second request limit reached"). - Premature Abort: Because
-32007is neither-32005,-32603, nor-1,shouldRetry(error)returnsfalse. Viem concludes that the error is deterministic (such as invalid parameters or invalid ABI encoding) and terminates the loop on the very first attempt.
Architectural Solutions to Fix -32007 Failures
Solution 1: Implement a Custom shouldRetry Predicate
Viem’s http() transport allows you to override the default retry logic by supplying your own shouldRetry handler.
Here is the production-grade implementation that intercepts QuickNode’s -32007 alongside standard EVM RPC errors:
import { createPublicClient, http, type RpcError } from 'viem';
import { mainnet } from 'viem/chains';
// Comprehensive retry predicate covering standard and provider-specific rate limits
export function isRetryableRpcError(err: unknown): boolean {
if (!err || typeof err !== 'object') return false;
const rpcErr = err as Partial<RpcError> & { status?: number; details?: string; message?: string };
// 1. QuickNode specific rate limit code
if (rpcErr.code === -32007) {
return true;
}
// 2. Standard Viem RPC transient codes
if (rpcErr.code === -1 || rpcErr.code === -32005 || rpcErr.code === -32603) {
return true;
}
// 3. HTTP status codes (for edge proxies)
if (rpcErr.status && [408, 413, 429, 500, 502, 503, 504].includes(rpcErr.status)) {
return true;
}
// 4. Fallback message inspection for nested provider errors
const message = (rpcErr.message || rpcErr.details || '').toLowerCase();
if (
message.includes('request limit reached') ||
message.includes('too many requests') ||
message.includes('rate limit exceeded')
) {
return true;
}
return false;
}
export const publicClient = createPublicClient({
chain: mainnet,
transport: http("https://your-node.quiknode.pro/your-token/", {
retryCount: 6,
retryDelay: ({ count }) => {
// Exponential backoff with jitter: 2^count * 250ms + random jitter
const baseDelay = Math.min(1000 * 2 ** count, 10_000);
const jitter = Math.floor(Math.random() * 200);
return baseDelay + jitter;
},
shouldRetry: ({ error }) => isRetryableRpcError(error),
}),
});
Solution 2: Resilient High-Availability Fallback Transport
Relying on a single RPC endpoint for automated agents or mission-critical bots introduces a single point of failure. If QuickNode’s plan quota is completely exhausted, retrying 6 times will merely delay failure.
The optimal design combines the patched shouldRetry logic with Viem’s fallback() transport across redundant node providers:
import { createPublicClient, fallback, http } from 'viem';
import { arbitrum } from 'viem/chains';
import { isRetryableRpcError } from './rpcUtils';
// Configure Primary (QuickNode)
const quickNodeTransport = http("https://arbitrum-mainnet.quiknode.pro/TOKEN/", {
name: 'QuickNode-Primary',
retryCount: 3,
retryDelay: 800,
shouldRetry: ({ error }) => isRetryableRpcError(error),
});
// Configure Secondary Backup (Alchemy)
const alchemyTransport = http("https://arb-mainnet.g.alchemy.com/v2/TOKEN/", {
name: 'Alchemy-Backup',
retryCount: 3,
retryDelay: 1000,
shouldRetry: ({ error }) => isRetryableRpcError(error),
});
// Configure Tertiary Public/Direct RPC
const publicBackupTransport = http("https://arb1.arbitrum.io/rpc", {
name: 'Public-Fallback',
retryCount: 2,
retryDelay: 1500,
});
export const resilientClient = createPublicClient({
chain: arbitrum,
transport: fallback([
quickNodeTransport,
alchemyTransport,
publicBackupTransport,
], {
rank: false, // Maintain priority order: Primary -> Backup -> Fallback
retryCount: 1,
}),
});
Defensive Client-Side Rate Limiter for AI Execution Agents
When LLM-driven agents trigger bulk batch operations (such as scraping eth_getLogs or resolving multiple contract states simultaneously), firing hundreds of parallel promises will inevitably overwhelm node concurrency caps.
Wrap call dispatches in a simple token-bucket or concurrency queue:
import pLimit from 'p-limit';
// Restrict RPC calls to 25 concurrent requests maximum
const rpcConcurrencyLimit = pLimit(25);
export async function safeBatchContractReads<T>(
calls: Array<() => Promise<T>>
): Promise<T[]> {
return Promise.all(
calls.map((call) => rpcConcurrencyLimit(() => call()))
);
}
Verification & Self-Test Routine
To verify that your Viem client handles -32007 gracefully, you can run a diagnostic script using a mock HTTP interceptor or by intentionally lowering your QuickNode plan throughput:
import { isRetryableRpcError } from './rpcUtils';
// Mock QuickNode -32007 payload
const mockQuickNodeError = {
name: 'RpcRequestError',
code: -32007,
message: 'N/second request limit reached - reduce calls per second or upgrade your account at quicknode.com',
};
console.assert(
isRetryableRpcError(mockQuickNodeError) === true,
'Validation Failed: -32007 should be classified as retryable'
);
console.log('✔ QuickNode -32007 retry predicate verified successfully.');
By explicitly expanding Viem’s shouldRetry predicate and layering redundant providers via fallback(), your Web3 agents and backend indexers will seamlessly absorb traffic surges without unexpected downtime.