For AI agents: an LLM-friendly Markdown version of every page is available by appending .md to its URL or by sending an Accept: text/markdown request header. The full documentation index is at https://www.ankr.com/docs/llms.txt
Skip to main content

Retry Strategies & Rate Limits

Transient errors are normal in distributed infrastructure. A resilient client retries the right errors with backoff and fixes — not retries — the rest. This page shows which is which and how to back off.

What to retry, what to fix​

Retry with backoff (server-side, 5xx)Fix first — don't retry (client-side, 4xx)
-32055 No nodes available-32049/-32050/-32051/-32060 API-key problems
-32056 Proxy error-32052/-32053/-32054 key not allowed (chain/method/IP)
-32057 Node non-success status-32602 Invalid params
-32059 Failed to dial node-32601 Method not found
-32061 No archive nodes available-32062 Request / block range / batch too large
-32063 Node returned unexpected error-32079/-32080 origin / contract not allowed
-32064 Retry failed
-32071 Request timeout
-32076 Invalid response
-32085–-32089 No alive WS nodes
-32090 Too many requests (429 — back off)

Rule of thumb: HTTP 5xx → retry; HTTP 4xx → fix. The one 4xx you do retry is 429 (-32090) — but only with backoff, never in a tight loop. See the full list in the Error Reference.

Exponential backoff with jitter​

Back off exponentially and add jitter so many clients don't retry in lockstep. Cap the delay and the attempt count.

JavaScript

const RETRYABLE = new Set([-32055, -32056, -32057, -32059, -32061, -32063, -32064, -32071, -32076, -32090]);

async function rpc(url, body, { maxRetries = 5, baseMs = 200, capMs = 10_000 } = {}) {
for (let attempt = 0; ; attempt++) {
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});

if (res.status === 429 || res.status >= 500) {
if (attempt >= maxRetries) throw new Error(`giving up after ${attempt} retries (HTTP ${res.status})`);
await sleepBackoff(attempt, baseMs, capMs);
continue;
}

const json = await res.json();
if (json.error && RETRYABLE.has(json.error.code) && attempt < maxRetries) {
await sleepBackoff(attempt, baseMs, capMs);
continue;
}
return json; // success, or a non-retryable error to handle
}
}

function sleepBackoff(attempt, baseMs, capMs) {
const exp = Math.min(capMs, baseMs * 2 ** attempt);
const delay = Math.random() * exp; // full jitter
return new Promise((r) => setTimeout(r, delay));
}

Python

import random, time, requests

RETRYABLE = {-32055, -32056, -32057, -32059, -32061, -32063, -32064, -32071, -32076, -32090}

def rpc(url, body, max_retries=5, base=0.2, cap=10.0):
for attempt in range(max_retries + 1):
res = requests.post(url, json=body)
if res.status_code == 429 or res.status_code >= 500:
if attempt == max_retries:
res.raise_for_status()
time.sleep(random.uniform(0, min(cap, base * 2 ** attempt)))
continue
data = res.json()
err = data.get("error")
if err and err.get("code") in RETRYABLE and attempt < max_retries:
time.sleep(random.uniform(0, min(cap, base * 2 ** attempt)))
continue
return data

Go

var retryable = map[int]bool{
-32055: true, -32056: true, -32057: true, -32059: true, -32061: true,
-32063: true, -32064: true, -32071: true, -32076: true, -32090: true,
}

func backoff(attempt int) time.Duration {
exp := math.Min(10_000, 200*math.Pow(2, float64(attempt))) // ms, capped at 10s
return time.Duration(rand.Float64()*exp) * time.Millisecond // full jitter
}

Honor 429 and Retry-After​

A 429 / -32090 means you're over your plan's request rate. Don't hammer — back off, and if a Retry-After header is present, wait at least that long. Sustained 429s mean it's time to batch requests or upgrade your plan.

Idempotency — what's safe to resend​

  • Reads (eth_call, eth_getLogs, eth_getBlockByNumber, …) are idempotent — always safe to retry.
  • eth_sendRawTransaction is safe to resend: the transaction is already signed, so resending the same bytes yields the same transaction hash — it can't double-spend. Treat "already known" / "nonce too low" on a retry as success (the first attempt landed).
  • Always propagate a fresh request and keep the returned trace_id from any error for support.

Connection management​

The connection-limit errors (-32067, -32068, -32069, -32077) mean too many concurrent connections, not too many requests. Reuse a connection pool / keep-alive instead of opening a socket per call, and for WebSockets reconnect with the same backoff after -32084 (IO error) or a dropped subscription.

FAQ​

How many times should I retry?​

Cap at ~5 attempts with exponential backoff (base ~200 ms, max delay ~10 s). Beyond that, surface the error with its trace_id.

Is it safe to retry a transaction send?​

Yes. eth_sendRawTransaction carries a signed transaction with a fixed hash, so resending can't create a duplicate. Handle "already known"/"nonce too low" as confirmation the original was accepted.

Why am I getting 429 even under my request limit?​

Check the connection-limit codes (-32067–-32077) — those cap concurrent connections, separately from request rate. Pool and reuse connections rather than opening one per call.