8 minIteradian Engineeringengineeringreliability

How to handle RPC rate limits without dropping user transactions

Every managed RPC provider throttles you eventually. The difference between a brief slowdown and a failed transaction is what your client does with an HTTP 429. This post covers Retry-After, exponential backoff with jitter, request batching and the idempotency trap around sending raw transactions.

Rate limiting is not a failure mode, it is a normal operating condition. Every shared RPC provider enforces a per-second ceiling, and every application will hit it during a spike. What separates a well-behaved client from a broken one is the ten lines of code that run after the first HTTP 429. Iteradian returns 429 with a JSON-RPC error body on every throttled request, and its data.retryAfter field says how many seconds to wait; most providers send a similar hint, often as a Retry-After header.

Read Retry-After before you invent a delay

The most common mistake is retrying on a fixed timer. If the provider tells you how long to wait, wait that long. A hardcoded 100ms retry against a limiter asking for 2 seconds simply generates four more rejections and burns your quota faster.

typescript
const endpoint = "https://api.iteradian.com/rpc/v1/eth-mainnet";
const key = process.env.ITERADIAN_API_KEY ?? "";

async function rpc(body: unknown, attempt = 0): Promise<unknown> {
  const res = await fetch(endpoint, {
    method: "POST",
    headers: { "Content-Type": "application/json", "X-Api-Key": key },
    body: JSON.stringify(body),
  });

  if (res.status !== 429) return res.json();

  // A 429 carries a JSON-RPC error body. Only -32005 (the per-second or
  // concurrency ceiling) clears by waiting; -32006 (monthly quota) and
  // -32007 (overage cap) never will, so retrying them only burns time.
  const { error } = await res.json();
  if (error?.code !== -32005) throw new Error(`limit reached (${error?.code}): ${error?.message}`);
  if (attempt >= 5) throw new Error("rate limited: giving up after 5 attempts");

  // Honour the server's instruction when it gives one (in seconds).
  const retryAfter = Number(error.data?.retryAfter ?? res.headers.get("retry-after"));
  const serverDelay = Number.isFinite(retryAfter) ? retryAfter * 1000 : 0;

  // Otherwise back off exponentially, with jitter so a fleet of workers
  // does not synchronise and retry in a single thundering herd.
  const backoff = Math.min(2 ** attempt * 250, 8000);
  const jitter = Math.random() * backoff;
  await new Promise((r) => setTimeout(r, Math.max(serverDelay, jitter)));

  return rpc(body, attempt + 1);
}

Jitter matters more than the backoff curve

If twenty workers all get throttled in the same second and all back off by exactly 500ms, they all retry in the same second again. Full jitter — a random delay between zero and the backoff ceiling — spreads the retry storm out and recovers far faster than a precise exponential curve without it.

Never blind-retry eth_sendRawTransaction

This is the one that costs real money. Read calls are idempotent, so retrying is free. Sending a raw transaction is not. If the request timed out rather than being rejected, the transaction may already be in the mempool, and a retry can produce a second broadcast.

  • A clean HTTP 429 means the request was rejected before reaching the node. It is safe to retry.
  • A timeout or a dropped connection means you do not know. Re-check by transaction hash before resending.
  • Because the transaction hash is deterministic for a signed payload, you can compute it locally and call eth_getTransactionByHash first.
  • Never re-sign with a fresh nonce on retry. That is how you end up with two transactions instead of one.

Batch reads instead of paying for round trips

JSON-RPC 2.0 supports array batching. Sending twenty reads as one array is one round trip instead of twenty, which cuts latency and connection overhead. It does not stretch your allowance: on Iteradian a batch of N calls counts as N requests against both the per-second limit and the monthly quota, and the responses can come back in any order, so match them by id. Keep batches modest — 10 to 50 calls is a reasonable range — because one very large batch can time out as a unit and cost you everything in it.

Separate your traffic classes

Backfills, indexers and user-facing reads should not share a rate-limit bucket. If your provider supports per-key limits, issue a separate key per traffic class and cap the background jobs below the user-facing ones. On Iteradian the plan's per-second ceiling, the concurrency ceiling and the monthly allowance are counted per organization and shared by every key you hold, but a key can carry its own lower per-second limit — so a backfill key can be capped below the rate your checkout flow needs.

Instrument the limiter, not just the errors

  1. Count 429s as a first-class metric, not as part of a generic error rate.
  2. Alert on the 429 rate crossing a threshold, not on the first occurrence — occasional throttling is normal.
  3. Track retry depth. If you regularly reach attempt 4, you are under-provisioned, not unlucky.
  4. Watch the ratio of 429s to successful calls per key, so you can see which traffic class is causing it.

If your retry depth is consistently high, no amount of backoff will fix it — that is a capacity problem, and the fix is a higher per-second ceiling or fewer calls per unit of work.

Frequently asked questions

01

What does HTTP 429 mean from an RPC provider?

HTTP 429 Too Many Requests means your request was rejected because you exceeded a limit on your plan or API key. The response usually says how long to wait before retrying — on Iteradian, in the data.retryAfter field of the JSON-RPC error body. Monthly-quota (-32006) and overage-cap (-32007) rejections will not clear by retrying.

02

Is it safe to retry a rate-limited transaction broadcast?

Retrying after a clean 429 is safe, because the request never reached the node. Retrying after a timeout is not, because the transaction may already be in the mempool. Check by transaction hash first, and never re-sign with a new nonce.

03

Why add jitter to exponential backoff?

Without jitter, every throttled client backs off by the same amount and retries in the same instant, recreating the overload. Random jitter spreads retries across the window and lets the system recover.

Keep reading

Start

Point one endpoint at Iteradian and watch where the traffic goes.

Create a key, send a JSON-RPC call, and read that request back in your analytics a second later. No migration, no contract.

Trial
7 days, no card required
Setup
One endpoint, one header
Commitment
Monthly or annual, cancel any time