Fixing RPC Rate Limits and 429 Errors: Retry, Backoff and Failover

Fixing RPC Rate Limits and 429 Errors: Retry, Backoff and Failover

If your application is getting rate limited on RPC calls, the fix is almost never “retry harder.” Persistent limiting means your request volume exceeds what your plan or your gateway will admit, and the only durable responses are to send fewer requests or to raise the ceiling. Retry logic exists to survive the brief, bursty case gracefully, not to grind through a sustained overage.

What makes this harder to debug than an ordinary HTTP problem is that rate limits in a blockchain stack live at several layers, and they do not all announce themselves the same way. A limit imposed by your gateway looks different from one imposed by the node provider behind it, and in JSON-RPC neither of them necessarily arrives as an HTTP 429.

  • In JSON-RPC, a rate limit frequently arrives as HTTP 200 with an error object. Code that only inspects status codes will not see it.
  • Read Retry-After or the equivalent field in the error payload before choosing your own delay. A server-supplied wait beats any backoff curve you invent.
  • Exponential backoff needs jitter, or your clients will synchronize and retry in a thundering herd.
  • An upstream provider’s own rate limit may be forwarded to you verbatim without triggering failover. This is the single most confusing case in the stack.

Three Places a Limit Can Come From

The first is your own client. Connection pool caps, worker concurrency and any client-side throttle you configured are all limits, and they are the only ones you can adjust instantly. Rule these out first. A queue backing up inside your own process is often mistaken for an upstream limit.

The second is the gateway or access layer, if you have one. This is typically where per-account, per-application and per-IP policies are enforced. On the Aurpay RPC Gateway, for example, JSON-RPC and the TRON HTTP API carry independent capacity, and limits can apply at global, IP, account and application scope simultaneously. That independence matters in practice: saturating one protocol does not necessarily explain errors on the other, and assuming a single shared budget sends you looking in the wrong place.

The third is the upstream node provider: Alchemy, QuickNode, Chainstack, dRPC or whoever actually serves the data. Their limits are enforced on their side and are usually the real constraint on a production workload. This is also the layer where the signal gets most confusing, for reasons covered further down.

Reading the Signal Correctly

The JSON-RPC 2.0 specification transports application errors inside the response body, not in the HTTP status. A conforming server can return HTTP 200 with an error object describing a rate limit, and that is entirely correct behavior. Client code written against REST conventions — check the status, throw on 4xx, otherwise parse — will treat that response as a success and hand a malformed result to whatever called it.

Inspect both. The reliable pattern is: check the HTTP status, then check whether the parsed body contains an error key, then branch on the numeric code inside it.

On the Aurpay gateway the concrete shapes are documented and worth using as a template for what to look for elsewhere. A JSON-RPC rate limit returns HTTP 200 with code -32029, a data.retry_after_ms field, and a Retry-After header. The TRON HTTP API, being a REST-style interface, uses the conventional HTTP 429 with a Retry-After header, following the standard 429 Too Many Requests semantics. Two protocols, two signalling conventions, one gateway, which is exactly why a status-code-only check is insufficient.

It is also worth separating rate limiting from the other errors it gets confused with. A response saying no endpoint is available is a routing or configuration problem, not a throttle. An authentication failure is not a throttle either, though both can appear suddenly under load when a key rotation coincides with a traffic increase. Branch on the specific code rather than treating every non-success as “the provider is limiting us.”

Back Off Properly

The correct retry sequence is: read the server’s suggested wait, honor it if present, otherwise fall back to exponential backoff with jitter, and cap both the delay and the number of attempts.

Honoring the server’s value first is the part most implementations skip. Retry-After or retry_after_ms reflects when capacity is actually expected to be available. A backoff curve is a guess, and a guess that fires earlier than the server’s own estimate simply consumes another rejection.

Jitter is the part that gets skipped second, and its absence is what turns a brief limit into a sustained outage. Pure exponential backoff is deterministic, so every client that was rejected at the same moment retries at the same moment, is rejected together, and retries together again. The herd stays synchronized indefinitely. Randomizing each delay across a range breaks the lockstep. Full jitter, where the delay is a random value between zero and the current ceiling, is the simplest version that works.

Cap the total attempts. Beyond three or four, retries are no longer smoothing over a transient condition; they are adding load to a system that has already told you it is over capacity. Fail the operation, surface it, and let a queue or a scheduled job pick it up later.

One architectural note that surprises people: a gateway that retries internally may not be adding backoff between its own attempts. The Aurpay gateway documents this explicitly: attempts run back to back with no wait and no jitter, and client-side backoff remains the caller’s responsibility. Do not assume that because something upstream retries, you can stop.

The Failure That Does Not Look Like a Failure

This is the case worth reading twice, because it explains a class of incident that otherwise makes no sense.

When a gateway forwards your request to an upstream provider and that provider returns a valid JSON-RPC error — HTTP 200 with an error object — the forward technically succeeded. The gateway asked, the upstream answered. Aurpay’s failsafe documentation states the consequence plainly: such a response is returned to you verbatim, with no retry, no failover to another endpoint, and no health penalty against the endpoint that produced it. The same treatment applies to execution reverted and to an upstream expressing its own rate limit.

The implication is that an upstream provider throttling you inside a 200 response will not cause your gateway to route around it. From the gateway’s perspective nothing failed. From your perspective, requests are being rejected while a perfectly healthy secondary endpoint sits unused.

Transport-level failures behave differently. A connection failure, an unavailable endpoint, or an upstream rejecting your credentials will move the request to the next candidate. An upstream returning HTTP 425 or 429 at the transport layer is also handled. It opens a short throttle on that endpoint, honors the upstream’s Retry-After, and is deliberately excluded from health scoring so a temporary throttle does not get recorded as an unhealthy endpoint.

So the practical question when you are being limited is: is the upstream telling you through the transport, or inside the response body? The first is handled for you. The second is yours to detect and act on, and detecting it means inspecting error codes in responses your monitoring probably counts as successful.

Send Fewer Requests First

Retry tuning treats the symptom. If limiting is persistent rather than bursty, the volume is the problem.

Start by finding duplicates. Identical requests issued within a window where the answer could not have changed are pure waste, and in most applications they are a substantial share of traffic. Caching immutable reads and coalescing concurrent identical calls typically removes more load than any retry strategy will ever save; we cover the specifics in cutting RPC costs with response caching.

Then look at your polling intervals. A loop asking for the chain tip every second on a network with twelve-second blocks spends most of its calls confirming that nothing changed. Match the interval to the block time, or move to a push mechanism where one is available.

Then look at what is calling. Health checks, monitoring probes, staging environments pointed at production credentials and long-forgotten cron jobs are routinely responsible for a large share of an RPC bill. Attribute traffic by caller before you assume the application is at fault.

Only after those three should you add capacity: a higher plan tier, or a second endpoint on the route so traffic has somewhere else to go. Spreading load across multiple upstreams is genuinely effective, but it works properly only when combined with the routing and failover behavior described in RPC failover and circuit breaking.

A Diagnostic Sequence

When limiting appears, work through it in this order rather than changing several things at once.

Confirm which layer is rejecting you by capturing a full failing response: status line, headers and body. The error code and the presence or absence of a Retry-After header usually identify the source immediately.

Check whether the rejection is at the transport layer or inside a 200 response, because that determines whether any failover you have configured is even eligible to engage.

Establish whether the condition is bursty or sustained. Bursts respond to backoff, coalescing and jitter. A sustained overage responds only to less traffic or more capacity, and treating it as a burst produces a retry storm.

Attribute the volume by caller and by method before changing plans. Buying capacity to serve requests that should not exist is the most expensive way to resolve this.

Frequently Asked Questions

Why am I getting HTTP 200 responses that are actually rate limits?

Because JSON-RPC carries application errors in the response body rather than in the HTTP status, and a rate limit is an application error under that model. It is correct protocol behavior. Your client must parse the body and check for an error object even when the status is 200.

What is the difference between -32029 and HTTP 429?

They express the same condition on different protocols. On the Aurpay gateway, JSON-RPC signals a rate limit with code -32029 inside a 200 response, carrying data.retry_after_ms and a Retry-After header; the TRON HTTP API uses a conventional HTTP 429 with Retry-After. Handle both if you use both protocols.

Should I retry immediately if the response includes Retry-After?

No. Wait for the interval the server specified. It reflects when capacity is expected to return, which your own backoff curve cannot know. Retrying earlier consumes another rejection and, if enough clients do it simultaneously, extends the condition.

Will a gateway automatically fail over when my provider rate limits me?

Only if the provider signals it at the transport layer. On the Aurpay gateway an upstream 425 or 429 opens a short throttle on that endpoint and moves traffic elsewhere, but an upstream rate limit expressed as a JSON-RPC error inside a 200 response is treated as a successful forward and returned verbatim, with no failover. Check how your own layer classifies this. It is rarely stated on a pricing page.

How many retries should I configure?

Three to four attempts with capped exponential backoff and jitter covers the transient cases. Beyond that you are adding load to a system that has already declined to serve you. Fail the operation and let a queue or scheduled job retry it on a longer horizon.

Does adding a second endpoint solve rate limiting?

It raises your effective ceiling, which helps if the limit is per-endpoint rather than per-account. It does not help if the constraint is at your gateway or account level, and it does nothing about duplicate traffic. Reduce the volume first, then add capacity.

Handle the Signal, Then Reduce the Volume

Most rate-limit incidents come down to two fixable things: clients that only inspect HTTP status codes and therefore miss half the rejections, and retry loops without jitter that keep a herd synchronized. Fix the detection, add jitter, honor Retry-After, and cap your attempts. Then go find the duplicate requests, because that is where the durable improvement is.

If you want the routing, retry classification and circuit behavior handled in one layer rather than reimplemented per service, the Aurpay RPC Gateway is free and open source under Apache-2.0, covering 20 chain and network combinations across Ethereum, Polygon, BNB Smart Chain, Arbitrum, Optimism, Base, Solana, Bitcoin, Litecoin and TRON. It documents its error contract in full, including the cases it deliberately does not handle for you, which is the part you actually need when something is failing at three in the morning.

Aurpaytech

The Aurpay team

Aurpay is a non-custodial crypto payment gateway helping merchants accept Bitcoin, Lightning, and stablecoin payments without giving up custody of their funds.