Limits & operations
Run aRPC Solana streams in production: the PPS disconnect, the filter-update ceiling, the 10-second heartbeat, and what sequence cannot tell you.
Four aRPC behaviours cause most production surprises: the PPS check closes the connection, filter updates have a hard rate ceiling, the heartbeat runs every 10 seconds, and sequence can never gap. Your connection and filter allowances come from the RPS on your plan; see Filter size.
The PPS check
The PPS condition lives on the aRPC transport and it is a gRPC condition, so there is no JSON-RPC error body to find and no error object on the stream. What you get is this:
| Field | Value |
|---|---|
| gRPC status | PERMISSION_DENIED (7) |
| Reason | permission_denied |
| Detail prefix | PPS limit requirement not met |
| Requirement | Your provisioned RPS + TPS ≥ 500 |
| Then | The server shuts the connection down |
Despite the name, this has nothing to do with how fast you are sending. The check runs once, at connection admission, and never measures your traffic at all: it compares the RPS and TPS on your plan against a required total of 500, the same threshold that gates aRPC access. What clears it is raising RPS or TPS until they total 500 in the dashboard: Starter (200 + 50) is below, Standard (500 + 125) and Professional (1,000 + 250) are above. See plan contents.
Match on status plus detail prefix rather than full-string equality, since a message may carry a trailing support sentence. A PERMISSION_DENIED without that prefix is something else, usually product access. See Gateway & streaming errors.
Filter update rate
Filter updates have a hard ceiling: above 50 filter-update messages per 1,000 ms the gateway rejects the stream with gRPC RESOURCE_EXHAUSTED (8), reason subscribe_rate_limit, detail filter update rate limit exceeded (50/1000ms). On v2, registrations and unregistrations count toward that window and pings don't; on legacy, every client message on the stream counts.
The pattern that works:
- Send your complete desired filter set as one
RegisterTransactionFiltersmessage, not one message per filter. - Record each
filter_idas pending, and clear it as itsFilterValidationResultarrives.
Two things never produce a result: an empty filter_id (that entry is skipped), and UnregisterTransactionFilters, which is never acknowledged. Keep the filter set you want on your side; it is also what you replay after a reconnect.
On legacy, an invalid Base58 key comes back as gRPC INVALID_ARGUMENT (3) and ends the stream.
Filter size
| Limit | Value |
|---|---|
account_include keys, summed across every active filter | RPS |
account_exclude keys, summed across every active filter | RPS |
account_required keys, summed across every active filter | RPS |
Each cap is a total rather than a per-filter allowance, and it is summed across your concurrent aRPC streams. NFT pass holders get a flat allowance instead of an RPS-derived one: 1,200 keys per selector on Pro and 2,000 on Elite.
If you need a wider set than your plan allows, we can widen it; ask us on Discord.
Go over a cap and the gateway denies that Subscribe message with gRPC RESOURCE_EXHAUSTED (8), reason filters, and a detail naming the selector, the count it saw and the limit, for example transactions account_include count exceeds limit (600/500).
Concurrent connections are a separate allowance: ceil(RPS × 5%), minimum 1, capped at 50.
Heartbeats
Every aRPC transaction stream gets a server heartbeat every 10 seconds, so your liveness deadline can come from a real number rather than a guess. On v2 SubscribeTransactions and SubscribeBinaryTransactions it arrives as a Pong with ping_id = 0; on legacy Subscribe as a SubscribeResponse whose only populated field is ping_id = 0. Match it by value when you need to tell it apart from a pong you asked for.
- Drop the stream yourself once nothing (no update, no heartbeat, no pong) has arrived within a comfortable multiple of 10 s.
- The server has its own 45-second upstream-silence timeout: it closes the connection when nothing has come down from the server side for 45 s while your client is still sending. If both directions go quiet the rule never fires, so it is not a substitute for your own client deadline.
- On entry streams, heartbeat messages are opt-in:
stream_options.include_heartbeat_updates = true.
Sequence numbers
sequence is initialised inside each v2 stream handler and incremented immediately before every message it emits, so on SubscribeTransactions, SubscribeBinaryTransactions and SubscribeSlots it is contiguous over everything that stream sends. You can never observe a gap there, and a discontinuity alert on it will only ever fire on your own bug.
SubscribeEntriesis the exception. With a default request the counter behaves like the others, but once you set anymetadata_filtervalue away from its default, every batch in which all entries pass the filter is emitted withsequence = 0, while heartbeats and partially-filtered batches keep incrementing. So don't read the entry stream'ssequenceas a counter at all.- It is connection-local and restarts on every new stream; four concurrent streams have four independent counters.
- It is not a replay cursor. Neither aRPC API has resume or replay, so persisting it buys you nothing.
- What it is good for is ordering and per-message log correlation. Reset your baseline on reconnect.
To detect real loss you need an external reference: compare against Solana RPC over a bounded window, or against a second stream.
Choose the right stream
| Use | When |
|---|---|
v2 SubscribeTransactions | You want structured fields, resolved lookup-table addresses, is_vote, fee_payer, and the matching filter_ids |
v2 SubscribeBinaryTransactions | You want exact wire bytes and decode them yourself; note it reports no matching filter IDs |
v2 SubscribeEntries | You need entry-level data, including is_last_in_slot |
v2 SubscribeSlots | You need slot lifecycle and current leader rather than inferring it from transaction traffic |
Legacy Subscribe | An existing integration; decoded transactions and observed slot numbers on one stream |
There is no replay, so anything you don't read off the stream is gone.
Access failures
| Status | Meaning | Before reconnecting |
|---|---|---|
UNAUTHENTICATED (16) | The token endpoint rejected x-token | Correct or refresh the token |
PERMISSION_DENIED (7) + PPS limit requirement not met | Plan RPS + TPS is below 500, connection closed | Raise RPS or TPS to total 500 |
| RPC fails immediately after connect | Egress IP is not allowlisted | Register the address traffic actually leaves from |
aRPC access is an IP allowlist by default, so adding a token will not fix an IP problem, and the tokenized URL path never applies here. Access & authentication has the full model.
Pre-execution data
Observation, not execution
aRPC shows you what it saw on the network before any of it executed. SLOT_STATUS_COMPLETE only means the slot's last entry was observed: not confirmed, not rooted, not finalized. Confirm by signature through Solana RPC before you do anything irreversible.