Summary
A 429 from a Solana JSON-RPC endpoint means the provider is throttling your request rate, not that your JSON payload is malformed. Python clients hit this most often when they poll in tight loops, batch too aggressively, or share one public endpoint across many workers. The fix is usually a mix of client-side backoff, request shaping, and moving to an endpoint with capacity that matches your workload. This article shows how to read the error, apply retries and jitter in Python, reduce request volume, and decide when a shared or dedicated endpoint is the right next step.
A 429 response from a Solana JSON-RPC endpoint is a rate-limit signal from the provider, not a problem with your JSON body. The server understood your request and chose to reject it because you exceeded an allowance tied to your API key, IP, or plan. In Python this usually appears as requests.exceptions.HTTPError: 429 Client Error: Too Many Requests or as a JSON-RPC error object with code -32429 on some providers. The practical question is not "what does 429 mean" but "what should I change next" — backoff, request shaping, or a different endpoint.
Diagnose first: is it rate limiting, bad JSON, or a dead endpoint?
Before you rewrite code, confirm what you are actually hitting. The three failures look similar in logs but need different fixes.
| Symptom in Python | Likely cause | First thing to check |
|---|---|---|
HTTP 429 with Retry-After or rate-limit headers | Provider throttling | Requests per second and burst size |
JSON-RPC error -32600 / -32700 | Malformed JSON or bad batch | json.dumps output and content type |
| Timeouts, connection resets, 5xx | Endpoint or network issue | Endpoint URL, DNS, provider status |
| 429 only under concurrency | Shared key or IP across workers | How many processes share one endpoint |
429 after a burst of getProgramAccounts | Heavy method cost | Method mix, not just request count |
A quick probe with curl isolates the endpoint from your Python code:
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://solana.api.onfinality.io/public \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"getHealth"}'
If a single request returns 200 but your script returns 429, the problem is your request pattern, not the endpoint. If even a single request returns 429, you are sharing a throttled key or IP with other traffic.
Reading the 429 response body correctly
Many Solana providers return a JSON-RPC error object rather than a plain HTTP error. Parse both layers in Python so you do not swallow the real cause.
import requests
ENDPOINT = "https://solana.api.onfinality.io/public"
payload = {"jsonrpc": "2.0", "id": 1, "method": "getSlot"}
resp = requests.post(ENDPOINT, json=payload, timeout=10)
if resp.status_code == 429:
retry_after = resp.headers.get("Retry-After")
print("Throttled. Retry-After:", retry_after)
print("Body:", resp.text[:300])
else:
data = resp.json()
if "error" in data:
print("JSON-RPC error:", data["error"])
else:
print("Slot:", data["result"])
Log the status code, the Retry-After header when present, and the JSON-RPC error code. That combination tells you whether the limit is per-second, per-minute, or method-weighted.
Fix 1: exponential backoff with jitter in Python
Retrying immediately makes throttling worse. Use exponential backoff with jitter, and cap the number of attempts so a persistent 429 fails loudly instead of hanging.
import random
import time
import requests
ENDPOINT = "https://solana.api.onfinality.io/public"
def rpc_call(method, params=None, max_attempts=5):
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []}
for attempt in range(max_attempts):
resp = requests.post(ENDPOINT, json=payload, timeout=10)
if resp.status_code == 429:
retry_after = resp.headers.get("Retry-After")
wait = float(retry_after) if retry_after else min(2 ** attempt, 30)
wait += random.uniform(0, 0.5) # jitter
time.sleep(wait)
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(data["error"])
return data["result"]
raise RuntimeError("Exceeded retry attempts after repeated 429s")
Jitter matters when many workers retry at once. Without it, your processes synchronize and re-trigger the same limit.
Fix 2: reduce request volume before you change providers
Throttling is often a symptom of inefficient polling. Before upgrading infrastructure, check whether you can send fewer, better requests.
- Batch independent calls. Solana JSON-RPC accepts arrays of requests. Batch
getSlot,getBalance, andgetAccountInfocalls that do not depend on each other. Keep batches modest — very large batches can be rejected or time out. - Use WebSocket subscriptions instead of polling.
slotSubscribe,accountSubscribe, andlogsSubscribepush updates overwss://solana.api.onfinality.io/public-wsand remove the need for tight polling loops. - Cache stable data. Token metadata, program IDs, and epoch info change slowly. Cache them instead of re-fetching per request.
- Avoid unbounded
getProgramAccounts. These are expensive. Filter withdataSliceandfiltersso the node does less work per call. - Stagger startup bursts. Services that fire hundreds of calls on boot look like an attack to a rate limiter. Add a small ramp-up delay.
Here is a batched call pattern:
batch = [
{"jsonrpc": "2.0", "id": 1, "method": "getSlot"},
{"jsonrpc": "2.0", "id": 2, "method": "getVersion"},
{"jsonrpc": "2.0", "id": 3, "method": "getHealth"},
]
resp = requests.post(ENDPOINT, json=batch, timeout=10)
for item in resp.json():
print(item.get("id"), item.get("result") or item.get("error"))
Fix 3: separate keys and workers
If several services, containers, or CI jobs share one API key or one public IP, they compete for the same allowance. Give each workload its own key where the provider supports it, and avoid running test suites against the same endpoint as production traffic. A single runaway loop can throttle everything behind the same key.
When to move off a shared public endpoint
Client-side fixes have limits. If you have already batched, cached, and backed off, and you still hit 429 during normal traffic, the endpoint's capacity no longer matches your workload. At that point the decision is about endpoint type, not code.
| Situation | Shared public endpoint | Dedicated node |
|---|---|---|
| Prototyping, low request volume | Usually fine | Overkill |
| Steady production traffic with bursts | May throttle under load | Predictable capacity |
Heavy getProgramAccounts or archive queries | Often throttled | Better suited |
| Strict latency or isolation needs | Shared resources | Isolated resources |
| Multiple teams on one key | Contention risk | Cleaner separation |
OnFinality provides Solana RPC through both a shared RPC API service and dedicated nodes when you need isolated capacity. You can review the Solana network page for supported transports and endpoint details, and compare plans on RPC pricing. If you are still evaluating options, the provider selection guide covers the criteria that matter for production workloads.
A minimal production-ready Python client
Combine backoff, batching, and a session with connection pooling. Reuse the session so you are not opening a new TCP connection per call.
import random
import time
import requests
class SolanaRPC:
def __init__(self, endpoint, max_attempts=5):
self.endpoint = endpoint
self.max_attempts = max_attempts
self.session = requests.Session()
def call(self, method, params=None):
payload = {"jsonrpc": "2.0", "id": 1, "method": method, "params": params or []}
for attempt in range(self.max_attempts):
resp = self.session.post(self.endpoint, json=payload, timeout=10)
if resp.status_code == 429:
wait = float(resp.headers.get("Retry-After", min(2 ** attempt, 30)))
time.sleep(wait + random.uniform(0, 0.5))
continue
resp.raise_for_status()
data = resp.json()
if "error" in data:
raise RuntimeError(data["error"])
return data["result"]
raise RuntimeError("Repeated 429 responses")
rpc = SolanaRPC("https://solana.api.onfinality.io/public")
print(rpc.call("getSlot"))
Monitoring so 429s do not surprise you
Track throttle rate as a first-class metric, not just an error count. Useful signals:
- 429 responses per minute, split by method.
- Retry attempts per successful call.
- p95 and p99 latency for the heaviest methods.
- WebSocket reconnect frequency if you use subscriptions.
Alert when 429s rise during normal traffic, not only during known spikes. A slow upward trend usually means your workload grew past the endpoint's allowance.
Key Takeaways
- A Solana 429 is a provider rate-limit signal, not a JSON formatting error.
- Parse both the HTTP status and the JSON-RPC error body in Python.
- Use exponential backoff with jitter and a retry cap.
- Reduce volume with batching, WebSocket subscriptions, caching, and filtered
getProgramAccounts. - Separate API keys and workers so one loop cannot throttle everything.
- If throttling persists after client-side fixes, evaluate a shared or dedicated endpoint that matches your workload.
Frequently Asked Questions
Does a 429 mean my JSON is invalid?
No. Invalid JSON usually returns a parse error such as -32700. A 429 means the request was understood but rejected due to rate limits.
How long should I wait before retrying?
Honor the Retry-After header when present. Otherwise start with a short delay and increase it exponentially, capped at a sensible maximum, with random jitter.
Can batching make 429s worse?
Large batches can count as many requests or exceed payload limits. Keep batches modest and split heavy methods into separate calls.
Should I switch providers the moment I see a 429?
No. First confirm it is not a client-side pattern issue. If throttling continues under normal, optimized traffic, then compare endpoint capacity and consider a dedicated node.
Do WebSocket subscriptions avoid 429s?
They reduce polling, which lowers request volume, but they have their own connection limits. Use them alongside backoff, not instead of it.