Sizing an RPC plan from your workload: sustained RPS, burst and heavy-method buckets
A request-per-second plan is sized differently from a credit plan. How to measure a workload, how a token bucket with 2× burst actually behaves, and a worked Solana indexer that lands on a specific tier.
Credit-based RPC pricing asks "how many calls will you make this month?". A plan limited by requests per second asks a different question: "how fast do you need to call, and which kinds of calls?". The second question is harder to answer from a dashboard of monthly totals, and easier to get wrong in both directions — overbuying because of a daily peak you could smooth, or underbuying because a reconnect storm is invisible in averages.
Credit-based providers publish per-method weights instead (Helius's credits page, read 2026-09-22, weights a standard call at 1 credit and a DAS call at 10). What is rarely published is a sizing walk-through for a per-second model. This post is one, using Triport's published plan matrix, and it ends with a concrete tier for a concrete workload. For the concepts behind credits versus flat pricing, see RPC credits vs flat pricing.
Measure your workload by category, not in total
An RPS plan does not have one budget. On Triport, every method belongs to a category — for example sol_read_rpc, sol_read_rpc_heavy or eth_read_rpc — and each plan sets a sustained RPS per category. The rate limits reference says each chain splits its methods into roughly eight buckets.
So the first measurement is a split. For a week of real traffic, count requests per second per category, and keep three numbers for each: the typical rate, the busiest sustained minute, and the busiest single second. Every response tells you which bucket it was charged to — the X-RateLimit-Category header names it — so you can classify from your own logs rather than from a method list.
On Solana the split that matters most is ordinary versus heavy reads:
| Category | Example methods | Trial | Basic | Pro | Business |
|---|---|---|---|---|---|
sol_read_rpc | getAccountInfo, getSignaturesForAddress, getTransaction, getBlock | 20 | 60 | 200 | 600 |
sol_read_rpc_heavy | getProgramAccounts | 2 | 5 | 20 | 80 |
eth_read_rpc (for comparison) | eth_call, eth_getLogs, eth_getBalance | 10 | 20 | 100 | 250 |
The heavy bucket is small on purpose. A getProgramAccounts scan can do far more work than an ordinary read, so it gets its own budget instead of a weight. That makes it the first bucket to check, not the last: one careless loop over getProgramAccounts exhausts it long before your ordinary reads notice. The Solana limits page lists the categories for the rest of the method set.
Enterprise contracts set no per-category cap by default. Everything below is about the four published plans.
Sustained vs. burst: how the token bucket behaves
The documented rule is short: burst capacity is 2× the sustained rate, and above that you receive 429 rate_limited with Retry-After: 1. The mechanism behind it is a token bucket:
- the bucket holds up to
2 × sustainedtokens and starts full; - it refills continuously at the sustained rate;
- each request spends one token; with none left, the request is rejected.
This model predicts behaviour exactly, which is why it is worth doing the arithmetic. With a sustained rate r and a demand d > r, a full bucket of 2r tokens drains at d − r per second, so it lasts 2r ÷ (d − r) seconds.
- Basic
sol_read_rpc, r = 60, capacity 120. A spike to 100 per second drains at 40 per second: 3 seconds, then429s. - The same spike to 100 on Pro (r = 200) never touches the bucket.
- A spike to 90 per second on Basic lasts 4 seconds; a spike to 70 lasts 12.
Burst is therefore good for exactly one thing: absorbing short, unplanned spikes — a retry wave after a brief network error, a page of users arriving at once. It is not capacity for sustained work. If your busiest minute is above your sustained rate, you are on the wrong plan or your client needs pacing.
There is also no daily quota: the reference states there is no per-day cap and no daily-quota header, and plans carry no credits, compute units or overage. A key that stays inside its per-second budgets can run all month.
Why heavy methods get their own budget
Two workloads with the same request rate can cost a node very different amounts. Rather than weighting each call, a per-category plan puts expensive methods in a separate, smaller bucket. The effect for you is that cheap calls cannot be starved by expensive ones, and vice versa: a burst of getProgramAccounts exhausts sol_read_rpc_heavy and returns 429s for those calls only, while getAccountInfo keeps working from sol_read_rpc.
It also changes how you design. A heavy call you make once a minute costs almost nothing against a 5-per-second budget; the same call made per user request does not fit any plan. Cache heavy results, refresh them on a timer, and serve reads from the cache.
Reading 429 and the rate-limit headers
A rejected request is a plain HTTP 429, not a JSON-RPC error, with a body that tells you exactly which budget you hit:
{
"error": "rate_limited",
"current_tier": "basic",
"category": "sol_read_rpc",
"limit_rps": 60,
"burst_capacity": 120,
"retry_after_sec": 1
}Successful responses carry the same information as headers — X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Category. Two details from production, both observed on 2026-09-22 with a key on a plan that has no per-category cap: the category header names the bucket (sol_read_rpc, eth_read_rpc, polygon_read_rpc), and the limit and remaining headers read -1, the gateway's value for "no per-second budget here". The same -1 appears on DAS calls on every plan, because DAS is gated by plan rather than metered per second.
A client that honours these needs three rules:
- On
429, waitRetry-Afterseconds and spread the retries out. Retrying every rejected request immediately turns a spike into a longer spike. - Pace below the sustained rate. Use
X-RateLimit-Remainingto slow down before the bucket is empty; a client-side token bucket set a little below your plan's sustained rate keeps the server-side bucket full for real spikes. - Count a JSON-RPC batch as its members. Size by the calls inside a batch, not by HTTP requests; batching saves round trips, not budget.
A worked example: a Solana indexer
A service indexes activity for 40 program-owned accounts and serves a dashboard. From a week of logs:
- it polls
getSignaturesForAddressfor each of the 40 accounts every 2 seconds: 20 per second; - it fetches each new transaction with
getTransaction, about 30 per second at the busiest sustained minute; - it refreshes three
getProgramAccountssnapshots every 10 seconds: 0.3 per second of heavy reads, arriving as 3 at once; - after a restart or reconnect it has a backlog of about 3,000 reads, which it clears at 50 per second on top of the steady 50: 100 reads per second for about a minute.
Checking each bucket against the plans:
| Need | Rate | Trial | Basic | Pro |
|---|---|---|---|---|
| Steady reads | 50 / s | 20 — no | 60 — yes, 10 of headroom | 200 — yes |
| Heavy snapshots | 3 at once, 0.3 / s | 2 (burst 4) — tight | 5 (burst 10) — yes | 20 — yes |
| Catch-up reads | 100 / s for a minute | no | burst lasts 3 s, then 429 | yes |
Basic runs this indexer day to day. What it cannot do is the catch-up: at 100 per second the bucket empties in 3 seconds and the rest of the minute is throttled. There are two honest answers. Either pace total reads to Basic's 60 per second — the steady load keeps 50, the backlog gets the remaining 10, and 3,000 backlog reads take about 5 minutes instead of 1 — for $20 a month; or buy Pro at $249, where 100 per second is half the sustained budget, because recovery time after an outage is worth more to you than the difference. That trade-off is the real sizing decision, and it is invisible if you only look at monthly totals.
The 7-day trial fits neither the steady load nor production use. It is a way to measure these numbers on your own traffic before choosing, limited to one key in the EU region.
When this is the wrong approach
- Your load is tiny and spiky. A few thousand calls a day with rare bursts may be cheaper on a small credit plan; per-second plans pay for readiness.
- You need one number for finance. RPS plans map poorly onto a per-call cost line. The pricing page shows what a plan costs if a category runs at its cap for a whole month, which is the closest equivalent.
- Transaction submission dominates. Sends are metered in their own buckets, which this example leaves out; on Solana, submission is not yet a generally available product on Triport.
- You have not measured. Estimates from method counts in code are usually off by the retry and reconnect behaviour you forgot. Measure a week first; the RPC cost calculator is for comparing plans once you have the numbers.
Sources
- Triport, rate limits and tiers: per-category sustained RPS, 2× burst,
429 rate_limitedbody fields,Retry-After: 1, rate-limit headers, no daily quota, 7-day EU-only trial with one token. - Triport, Solana limits and Ethereum limits:
sol_read_rpc20 / 60 / 200 / 600,sol_read_rpc_heavy2 / 5 / 20 / 80,eth_read_rpc10 / 20 / 100 / 250 (trial / Basic / Pro / Business). Plan prices from pricing: Basic $20, Pro $249. - Triport gateway: the burst is a token bucket with capacity equal to twice the sustained rate, refilled at the sustained rate. Rate-limit headers observed on production on 2026-09-22 (
X-RateLimit-Categorypresent;-1on a key with no per-category cap and on DAS calls). - Helius, Credits — https://www.helius.dev/docs/billing/credits, read 2026-09-22 (1 credit per standard call, 10 per DAS call).
- Arithmetic: 40 ÷ 2 = 20; 20 + 30 = 50; 3 ÷ 10 = 0.3; backlog 50 × 60 = 3,000; bucket life = 2r ÷ (d − r): 120 ÷ 40 = 3, 120 ÷ 30 = 4, 120 ÷ 10 = 12; paced catch-up on Basic 3,000 ÷ (60 − 50) = 300 seconds.