Rate limits & batching
Limits are enforced per API key, so a runaway job on a staging key cannot exhaust production capacity.
Plan limits
| Plan | Requests / second | Requests / month | Concurrent sockets |
|---|---|---|---|
| Free | 25 | 5,000,000 | 5 |
| Growth | 250 | 150,000,000 | 20 |
| Scale | 1,000 | 1,000,000,000 | 100 |
| Enterprise | Negotiated | Negotiated | Negotiated |
Testnet keys have their own allowance and never draw on your mainnet quota.
Rate-limit headers
Every response carries your current position:
X-Prism-RateLimit-Limit: 250
X-Prism-RateLimit-Remaining: 243
X-Prism-RateLimit-Reset: 1Reset is seconds until the window refills. When you exceed the limit you get 429 with a Retry-After header:
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": -32005,
"message": "Rate limit exceeded. Retry after 1s."
}
}Backing off correctly
Honour Retry-After. Retrying immediately on 429 makes the queue longer for everyone including you:
async function callWithBackoff(body, attempt = 0) {
const response = await fetch(PRISM_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get('Retry-After') ?? 1);
// Full jitter: spread retries out instead of synchronising every client.
const delay = retryAfter * 1000 * Math.random();
await new Promise((resolve) => setTimeout(resolve, delay));
return callWithBackoff(body, attempt + 1);
}
return response.json();
}Note this is backoff on your own rate limit - not a retry loop for upstream failures. Those are already handled and adding your own multiplies them.
What counts against your quota
Each JSON-RPC call counts as one request, including each entry in a batch. A batch of 20 costs 20.
These do not count:
- Requests served from the deterministic-result cache
- Requests collapsed into an identical in-flight call by deduplication
- Prism's own health probes against upstreams
- Retries Prism performs internally after an upstream failure
That last point matters: when Prism retries your read on a second provider, you are charged once. You are not billed for our failover.
Batching
Send an array instead of an object. Prism may route entries to different providers and returns them in request order:
const response = await fetch(PRISM_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify([
{ jsonrpc: '2.0', id: 1, method: 'eth_blockNumber', params: [] },
{ jsonrpc: '2.0', id: 2, method: 'eth_getBalance', params: [address, 'latest'] },
{ jsonrpc: '2.0', id: 3, method: 'eth_chainId', params: [] },
]),
});
const results = await response.json();Batch limits:
| Limit | Value |
|---|---|
| Entries per batch | 100 |
| Request body size | 10 MB |
| Response body size | 50 MB |
A batch fails as a batch only on a malformed envelope. Otherwise each entry succeeds or fails on its own, so always check every entry rather than assuming the batch succeeded:
for (const entry of results) {
if (entry.error) {
console.error(`request ${entry.id} failed:`, entry.error.message);
}
}Reducing request volume
Before asking for a higher limit, check for these:
Poll on newHeads instead of on a timer. A WebSocket subscription tells you when state actually changed, instead of asking every two seconds whether it has.
Turn on client batching. batch: true in viem or batchMaxCount in ethers collapses per-component reads into one round trip.
Cache what does not change. Token decimals, symbols and contract code are permanent for a deployed contract. Read them once at startup.
Pin historical reads to a block number. eth_call at latest is cached for two seconds; the same call at an explicit block is cached permanently.
Requesting more
Enterprise limits are set per account. Write to hello@prismrpc.co with your expected requests per second, method mix and regions, and we will size a pool for it.
