# Limits & operations (/arpc/limits)

> 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](#the-pps-check) closes the connection, [filter updates](#filter-update-rate) have a hard rate ceiling, [the heartbeat](#heartbeats) runs every 10 seconds, and [`sequence`](#sequence-numbers) can never gap. Your connection and filter allowances come from the RPS on your plan; see [Filter size](#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](https://dashboard.corvus-labs.io): Starter (200 + 50) is below, Standard (500 + 125) and Professional (1,000 + 250) are above. See [plan contents](/platform/pricing).

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](/solana-rpc/errors#pps-limit-requirement-not-met).

## 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

| 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](/platform/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](https://discord.gg/corvus-labs).

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

| Use                              | When                                                                                                               |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| v2 `SubscribeTransactions`       | You want structured fields, resolved lookup-table addresses, `is_vote`, `fee_payer`, and the matching `filter_id`s |
| 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](/platform/access) has the full model.

## Pre-execution data

<Callout type="warn" title="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.
</Callout>
