-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Description
Check existing issues
- I checked there isn't already an issue for the bug I encountered.
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
- 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),
});- Make multiple RPC calls that would trigger rate limiting
- Observe that HTTP requests return 200 OK but contain RPC errors
- Notice no retry attempts are made (only one
onFetchRequestlog)
🔧 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 attemptsLink 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 usingbatch: true,retryCountonly 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: