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_sendRawTransactionis 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_idfrom 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.