LayerZeroFault
ai agents-api Updated:

Viem shouldRetry QuickNode -32007 Rate-Limit Error Fix

VV

Written by

Updated:

Technical Guide

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:

  1. HTTP Status 200 OK: QuickNode’s proxy returns standard HTTP 200 OK status headers for rate-limited queries, returning the error inside the JSON-RPC response payload. Hence, the status === 429 check inside HttpRequestError is never triggered.
  2. Proprietary RPC Code: The JSON-RPC specification (RFC-7693) standardizes -32005 as “Limit exceeded”. However, QuickNode utilizes -32007 specifically to distinguish credit/rate threshold boundaries ("N/second request limit reached").
  3. Premature Abort: Because -32007 is neither -32005, -32603, nor -1, shouldRetry(error) returns false. 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.

Interactive Diagnostic Sandbox

Local Agent API & Calldata Sandboxes

Inspect autonomous agent UserOperations, decode packed execution calldata, and test programmatic authentication payloads locally.

Companion Diagnostic Guide Viem EIP-712 Nested Struct Hash Mismatch Revert Fix →

Fix signature verification reverts in Viem when encoding nested EIP-712 structs. Re-align type hashes, match primaryType definitions, and fix domain separators.

Read Guide
Contextual Architecture

Related Diagnostic Guides

View All in Silo →

Related Inquiries

Why doesn't Viem retry QuickNode error -32007 automatically?

Viem's default shouldRetry logic only recognizes standard HTTP status 429 and standard JSON-RPC -32005 rate-limit errors. QuickNode returns a proprietary -32007 error code, which Viem categorizes as an unhandled application error.

How do I configure a custom retry handler in Viem?

In your createPublicClient or http() transport setup, pass a custom shouldRetry callback that inspects err.code === -32007 and returns true to trigger exponential backoff.

What retry delay should be used for rate-limited RPCs?

Implement exponential backoff with jitter, starting at 1,000ms with a multiplier of 2.0 and a maximum retry count of 5 to absorb burst traffic without disconnecting.

How can I verify transaction calldata and gas limits when rate limits occur?

Use our interactive ERC-4337 Calldata Decoder tool to inspect packed transaction calls and gas parameters before sending them to the RPC.