Appearance
Batches, retries & caching
Batch requests
JSON-RPC chains accept arrays; elements are processed, cached and metered individually, and correlated by id (servers may reorder):
bash
curl -X POST https://rpc.lab.au.ro/eth \
-H "apikey: $YOUR_API_KEY" -H 'Content-Type: application/json' \
-d '[{"jsonrpc":"2.0","method":"eth_chainId","params":[],"id":1},
{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":2},
{"jsonrpc":"2.0","method":"eth_gasPrice","params":[],"id":3}]'Guidelines:
- Keep batches ≤ 50 items — one slow element delays the whole response.
- Every element pays its own CU; a batch is not cheaper, only fewer round-trips.
- ethers v6 batches automatically (
JsonRpcProvidercoalesces calls in a 10 ms window) — it works against the gateway out of the box.
Retries with exponential backoff + jitter
Retry only what's retriable (429 honoring Retry-After, 5xx, -32603 backend request failed — see the retry matrix). Never retry sends blindly: a broadcast can succeed even when the response times out — re-broadcasting the same signed bytes is safe (same txid), re-signing with a new nonce is not.
js
// fetchWithRetry: 429-aware, exponential backoff with full jitter.
async function fetchWithRetry(url, options, { tries = 4, baseMs = 500 } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, options)
if (res.status !== 429 && res.status < 500) return res
if (attempt >= tries - 1) return res
const retryAfter = Number(res.headers.get('retry-after'))
const delay = retryAfter
? retryAfter * 1000 // server knows best
: Math.random() * baseMs * 2 ** attempt // full jitter
await new Promise(r => setTimeout(r, delay))
}
}
// usage
const res = await fetchWithRetry('https://rpc.lab.au.ro/eth', {
method: 'POST',
headers: { apikey: process.env.YOUR_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_blockNumber', params: [], id: 1 })
})The same shape in Python:
python
import os, random, time, requests
def post_with_retry(url, json_body, tries=4, base=0.5):
for attempt in range(tries):
res = requests.post(url, json=json_body,
headers={"apikey": os.environ["YOUR_API_KEY"]}, timeout=30)
if res.status_code != 429 and res.status_code < 500:
return res
if attempt == tries - 1:
return res
retry_after = res.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else random.random() * base * 2 ** attempt)
return resIdempotency
- Reads are naturally idempotent — retry freely within the matrix.
- Broadcasts: the raw signed transaction is the idempotency key. Resubmitting identical bytes is safe on every chain we serve (nodes dedupe by hash); EVM nodes answer
already known— treat it as success. - Management API (
POST /keys, webhooks): not idempotent — creating twice makes two resources. On a timeout, list then reconcile instead of blind-retrying.
Client-side caching
The gateway already caches per method (table) — don't duplicate short-TTL caches client-side. Two things worth caching in your app:
- Static facts (
eth_chainId, decimals of tokens you touch): cache forever, they're also 0 CU. - Your own derived state (parsed blocks, computed balances): cache by
block_hash, not block number — reorg-safe.
X-Cache: HIT|MISS on every response tells you what the gateway did.