Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Bug Report: retryCount doesn't work with batch requests for RPC-level errors #3680

@wmzy

Description

@wmzy

Check existing issues

Viem Version

2.30.2

Current Behavior

When using HTTP transport with batch: true and retryCount configured, RPC-level errors (such as rate limiting, internal errors) do not trigger the retry mechanism, even though HTTP-level errors do.

Real-world scenario with Alchemy:

const client = createPublicClient({
  transport: http('https://eth-mainnet.g.alchemy.com/v2/YOUR_API_KEY', {
    batch: { batchSize: 10 },
    retryCount: 8,
    retryDelay: 5000,
  }),
  batch: { multicall: true },
});

When hitting Alchemy's rate limits, you get:

{
  "jsonrpc": "2.0", 
  "error": {
    "code": 429,
    "message": "Your app has exceeded its compute units per second capacity. If you have retries enabled, you can safely ignore this message. If not, check out https://docs.alchemy.com/reference/throughput"
  }
}

The Problem:

  • HTTP network errors (connection timeout, DNS failure): Will retry
  • Alchemy rate limiting (JSON-RPC error code 429): Will NOT retry in batch mode
  • Other RPC errors (rate limiting -32005, internal error -32603): Will NOT retry

Expected Behavior

retryCount should retry all transient errors, including RPC-level errors, regardless of whether batching is enabled or not.

🔍 Root Cause Analysis

After investigating the viem source code, the issue is in how batch requests are handled:

Single Request (Works correctly):

RPC Error → HTTP 200 with {"error": {...}} → Rejected Promise → Triggers retry ✅

Batch Request (Broken):

Batch with partial RPC errors → HTTP 200 with mixed results → No retry ❌
[
  {"id": 1, "result": "0x12345..."},
  {"id": 2, "error": {"code": 429, "message": "Your app has exceeded its compute units per second capacity..."}},
  {"id": 3, "result": "0x67890..."}
]

Key Issue: Alchemy's documentation explicitly states "If you have retries enabled, you can safely ignore this message" - but viem's retry mechanism doesn't work for RPC-level errors in batch mode!

The HTTP transport's retry logic only considers HTTP status codes, not RPC-level errors within successful batch responses.

Steps To Reproduce

  1. Enable batch processing with retry configuration:
const transport = http(RPC_URL, {
  batch: { batchSize: 10 },
  retryCount: 5,
  retryDelay: 1000,
  onFetchRequest: (req) => console.log('Request:', req.url),
  onFetchResponse: (res) => console.log('Response:', res.status),
});
  1. Make multiple RPC calls that would trigger rate limiting
  2. Observe that HTTP requests return 200 OK but contain RPC errors
  3. Notice no retry attempts are made (only one onFetchRequest log)

🔧 Minimal Reproducible Example

You can test this using Alchemy's low-throughput test endpoint designed specifically for testing rate limits:

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

// Using Alchemy's test API key with 50 CU/second limit
// From: https://docs.alchemy.com/reference/throughput#test-throughput--retries
const client = createPublicClient({
  chain: mainnet,
  transport: http('https://eth-mainnet.g.alchemy.com/v2/J038e3gaccJC6Ue0BrvmpjzxsdfGly9n', {
    batch: { batchSize: 10 },
    retryCount: 5,
    retryDelay: 1000,
    onFetchRequest: () => console.log('HTTP Request made'),
    onFetchResponse: (res) => console.log('Response status:', res.status),
  }),
  batch: { multicall: true },
});

// This will quickly hit the 50 CU/second limit and generate 429 errors
// But won't retry RPC-level 429 errors in batch responses
const promises = Array.from({ length: 50 }, (_, i) => 
  client.getBlockNumber()
);

await Promise.allSettled(promises);
// Expected: Only 2-3 "HTTP Request made" logs (no retries)
// Should see: 429 RPC errors without retry attempts

Link to Minimal Reproducible Example

No response

Anything else?

💡 Proposed Solutions

Option 1: Add RPC-level retry support

transport: http(RPC_URL, {
  retryCount: 5,
  retryRpcErrors: true,  // New option
  retryableRpcCodes: [429, -32603, -32005, -32000], // Include Alchemy's 429 code
})

Option 2: Better documentation

Clearly document this limitation in the HTTP transport docs:

⚠️ Note: When using batch: true, retryCount only applies to HTTP-level errors. RPC-level errors within batch responses are not retried.

Option 3: Consistent behavior

Make batch and non-batch requests behave consistently by implementing application-level retry for RPC errors.

🔄 Workaround

Currently, the only workaround is to disable batching:

const client = createPublicClient({
  transport: http(RPC_URL, {
    batch: false,  // Disable to enable RPC-level retries
    retryCount: 5,
  }),
  batch: { multicall: false },
});

📚 Additional Context

This inconsistency affects many users who expect retry behavior to be consistent. The issue is particularly problematic for:

  • Applications using Alchemy (most popular Ethereum RPC provider) with rate limits
  • Long-running indexers that need robust error recovery
  • High-throughput applications that rely on batch processing for performance
  • Developer confusion: Alchemy's docs say "If you have retries enabled, you can safely ignore this message" but viem's retries don't work!

🏷️ Impact

  • Severity: Medium-High
  • Scope: All users using batch requests with retry configuration
  • Frequency: Occurs whenever RPC providers return rate limiting or temporary errors

Environment:

  • Node.js: v18+
  • viem: 2.30.2
  • RPC Provider: Alchemy (but affects all providers)
  • Platform: Any

References:

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions