Corvus Labs

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:

FieldValue
gRPC statusPERMISSION_DENIED (7)
Reasonpermission_denied
Detail prefixPPS limit requirement not met
RequirementYour provisioned RPS + TPS ≥ 500
ThenThe 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:

  1. Send your complete desired filter set as one RegisterTransactionFilters message, not one message per filter.
  2. Record each filter_id as pending, and clear it as its FilterValidationResult arrives.

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

LimitValue
account_include keys, summed across every active filterRPS
account_exclude keys, summed across every active filterRPS
account_required keys, summed across every active filterRPS

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.

  • SubscribeEntries is the exception. With a default request the counter behaves like the others, but once you set any metadata_filter value away from its default, every batch in which all entries pass the filter is emitted with sequence = 0, while heartbeats and partially-filtered batches keep incrementing. So don't read the entry stream's sequence as 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

UseWhen
v2 SubscribeTransactionsYou want structured fields, resolved lookup-table addresses, is_vote, fee_payer, and the matching filter_ids
v2 SubscribeBinaryTransactionsYou want exact wire bytes and decode them yourself; note it reports no matching filter IDs
v2 SubscribeEntriesYou need entry-level data, including is_last_in_slot
v2 SubscribeSlotsYou need slot lifecycle and current leader rather than inferring it from transaction traffic
Legacy SubscribeAn 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

StatusMeaningBefore reconnecting
UNAUTHENTICATED (16)The token endpoint rejected x-tokenCorrect or refresh the token
PERMISSION_DENIED (7) + PPS limit requirement not metPlan RPS + TPS is below 500, connection closedRaise RPS or TPS to total 500
RPC fails immediately after connectEgress IP is not allowlistedRegister 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.

On this page