Skip to content

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 (JsonRpcProvider coalesces 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 res

Idempotency

  • 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.