Skip to main content
Solana gRPC streaming carries account, transaction, slot, entry and block metadata updates over one long-lived connection. The wire schema is stock Yellowstone, also called Dragon’s Mouth, at proto 12.5. There are no custom extensions, no added fields and no added methods, so any standard Yellowstone client connects unchanged and there is nothing proprietary to install.

Access

Solana gRPC streaming is available as an add-on. Onboarding runs through the Uniblock team: provisioning is handled per customer, and the endpoint and credential are issued during setup.

Contact sales

Onboarding for the Solana gRPC add-on runs through the Uniblock team.

Pricing

Solana gRPC streaming is billed on data transferred, at $50 USD per TB, with partial terabytes rounded up. Enterprise discounts are available. Replayed data is metered the same as live data, so a full-window catch-up bills like the equivalent live stream. Narrow filters and accounts_data_slice are the levers that reduce transferred volume.

Connect

Endpoint

TCP 443, HTTP/2. One hostname serves both live and historical subscriptions.

Authentication

Solana gRPC does not use the Uniblock API key. Streaming traffic does not pass through the Uniblock HTTP gateway, and the credential is a dedicated token issued during onboarding. The X-API-Key header that authenticates the Unified API, the Unified JSON-RPC endpoint and the Direct APIs has no effect on this endpoint.
The credential is a single bearer token carried in gRPC metadata under x-token. There is no mTLS, no basic auth, no signing step and no handshake exchange. Authorization is evaluated once, when the stream is established, and is not re-checked while a stream runs. The token is opaque, with no meaningful internal structure. An IP allow-list is available and is configured during onboarding. It is empty by default, so a token authenticates from any address unless addresses are added.

TLS

TLS only, on 443, against a public CA chain. No client certificate and no mTLS. The system trust store is sufficient.

Stubs and client libraries

Stubs are generated from the upstream Yellowstone repository at github.com/rpcpool/yellowstone-grpc. Uniblock distributes no .proto of its own. geyser.proto declares package geyser and imports solana-storage.proto publicly, so generating from geyser.proto alone pulls in both. Stubs generated from the earlier v11 protos are wire-identical across all 26 streaming messages and remain compatible.

What streams

Five update kinds are produced: accounts, transactions, slots, entries and block metadata, at PROCESSED, CONFIRMED or FINALIZED. Each update is delivered once. Slot status transitions and block metadata each arrive a single time per slot. Every update carries filters, the list of filter names that matched it, and created_at, a server-side google.protobuf.Timestamp. One update lists every filter name it matched, so a single message can satisfy more than one filter.
Two filters are accepted and never produce data. blocks and transactions_status are validated and held on the subscription, and neither ever fires. A subscription using only those filters receives nothing and reports no error. transactions covers the transaction status case. blocks_meta combined with transactions covers the block case.

Methods

All eight RPCs are implemented. Subscribe is the bidirectional stream. The seven unary methods answer from the same data the stream is built on, so a client holding a stream reads a blockhash or a slot without opening a JSON-RPC connection.

Filters

Each filter map key is an independent entry. An update is delivered if any entry matches it fully, and within one entry every populated field must hold. Sub-filters belong to their own entry and never gate frames matched by a different entry. The consequence in practice: a subscription carrying thirty owner filters plus one narrow memcmp filter receives everything the thirty owners produce, plus the memcmp matches. The memcmp does not narrow the other thirty. Constraints that must apply together belong in a single entry. The map key is a label. It comes back in filters to tell streams apart and is never compared against chain data. The addresses do the matching.

Transaction filters

SubscribeRequestFilterTransactions: An unset field is not a constraint, and an empty list is the same as unset. Leaving every field unset is a wildcard that streams every transaction, which is permitted and is how a firehose subscription is expressed. account_include is an OR across addresses. account_required is the AND version, which suits pinning a program and pool pair. Vote transactions dominate the raw feed by volume, so vote: false and failed: false is the usual starting point.

Account filters

SubscribeRequestFilterAccounts: Sub-filters, from SubscribeRequestFilterAccountsFilter: memcmp requires decoding account data and is the expensive one. owner or datasize as the first cut, with memcmp refining inside the same entry, costs less.

Slot, entry and block metadata filters

SubscribeRequestFilterSlots takes filter_by_commitment, which delivers only slot updates whose status matches the requested commitment, and interslot_updates, which controls whether the intermediate transitions arrive. SubscribeRequestFilterEntry and SubscribeRequestFilterBlocksMeta are empty messages, so a named entry subscribes to the whole kind.

Encoding

Pubkeys, owners, signatures and hashes are raw bytes on the wire. Filters are given as base58 strings. Forgetting to encode when logging and decode when filtering is the most common cause of a filter that matches nothing.

Reducing payload size

accounts_data_slice is a repeated {offset, length} on the request rather than on a filter. When present, account updates are re-encoded so data carries only the requested ranges. For a subscription watching one field in a large account layout this is the difference between kilobytes and tens of bytes per update, and it is the cheapest bandwidth reduction available.

Cardinality limits

Checked when a SubscribeRequest arrives, on live and historical subscriptions alike. Exceeding one ends the stream with INVALID_ARGUMENT and a message naming the limit. The 64 counts distinct named map keys rather than addresses, so one entry holding several thousand pubkeys is one entry. The 100,000 ceiling is theoretical, because a request that large exceeds the 4 MB message cap first.

Rejection rather than silent widening

An address that fails base58 decoding, a signature of the wrong length, a lamports sub-filter with no comparator, a datasize given twice, an oversized memcmp payload and an unsupported token_account_state all end the subscription with INVALID_ARGUMENT and a message naming the offending field and value. A filter entry that lost its only address constraint would be a wildcard over the whole kind, so the request fails rather than quietly turning a narrow subscription into a firehose.

A worked request

Non-vote, non-failed transactions touching two programs, plus slot updates:
Updates arrive with filters: ["pump-amm"], filters: ["raydium-v4"] or filters: ["slots"]. A transaction touching both programs lists both names.

Commitment and slot status

Commitment is set once per request through the commitment field and applies to the whole subscription. Three levels are accepted: PROCESSED, CONFIRMED and FINALIZED. Commitment promotion is buffered per slot rather than re-fetched, so a CONFIRMED or FINALIZED subscriber receives the same bytes the PROCESSED subscriber saw, promoted when the status transition arrives. A slot’s fate is reported by SubscribeUpdateSlot.status: Slots are never retracted and a replaced slot is not re-sent. A fork-aware client follows these transitions, treats anything below SLOT_FINALIZED as provisional, and unwinds work built on a slot that reports SLOT_DEAD. interslot_updates on a slot filter delivers the intermediate transitions rather than only the commitment-level ones. Each status fires once per slot.

Historical replay

Replay is triggered by setting from_slot on a SubscribeRequest. It is field 11 of the stock upstream message, so no special client, no SDK and no separate endpoint is involved. The stream replays forward from that slot and tails into live on the same connection, with no second reconnect and no gap. SubscribeReplayInfo returns first_available, the oldest slot currently held. The window drifts above the 48 hour floor because pruning runs on a schedule rather than instantly, so first_available is the value to branch on in code and 48 hours is the figure to plan against. The 64 session limit applies per customer rather than across the platform. Beyond it, a new replay subscription receives grpc-status: 8 and a retry message. Live subscriptions are never affected by replay load. Account updates are not retained. A from_slot subscription carrying an accounts filter returns nothing for the historical portion and begins delivering at the live edge. Transactions, entries, blocks and block metadata are retained. Every filter applies to replayed data exactly as it applies to live data. Addresses, owners, data size, lamports, the vote and failed flags, memcmp sub-filters and exact signature matching all behave identically on either path. A from_slot older than the retained window is rejected with grpc-status: 11 and a message naming the oldest slot still held. The available window is read out of that message rather than by retrying the same slot, because a reconnect loop that always sets the same from_slot after a rejection spins forever. The 10x cap is a real constraint on catching up. A full day of history takes roughly two and a half hours to replay. A subscription that needs to be current quickly starts from a nearer from_slot.

Limits and client behavior

Message sizes

A SubscribeRequest over 4 MB is rejected and the stream ends with a status rather than staying open. Because outbound messages are not capped, the binding constraint is the client’s own decode limit. Most gRPC stacks default to 4 MB. A subscription covering large accounts or busy blocks raises max_decoding_message_size on the Yellowstone client builder rather than relying on that default.

Subscriptions and connections

One SubscribeRequest per stream, replaceable in place: sending another replaces the active filter without reconnecting. A request carrying only ping is not treated as a filter update. There is no per-credential connection cap, and max_concurrent_streams is unset, so subscriptions multiplex freely over one connection.

Compression

Compression is not enabled and not negotiated. The server advertises no accepted or sent encodings, so every stream is uncompressed protobuf regardless of what a client offers in grpc-accept-encoding.

Keepalives

A SubscribeUpdate carrying UpdateOneof::Ping arrives on every stream every 15 seconds, including idle ones, so a filtered subscription that matches nothing still survives NAT and load-balancer idle timeouts. It is a keepalive and does not count as data. Keepalives carry created_at like every other update, so staleness computed from that field needs no special case. There is no server-side idle timeout. A stream lives until the client ends it, the connection breaks, or the client falls behind. Ping and Pong also exist as a client-initiated pair: a SubscribeRequest carrying only ping with an id returns the same id as SubscribeUpdatePong.

Falling behind

Drops are not signaled. There is no dropped-message count, no gap marker and no error status. When a client cannot keep up, its queue fills, the server removes it, and the stream simply ends. A client that logs only gRPC errors sees nothing at all, so an unexpected clean end of stream means the client fell behind rather than a normal shutdown.
Ordinary HTTP/2 flow control applies first. A slow reader stalls its own stream and affects neither other streams on its connection nor other subscribers. The stream closes only once the per-client queue fills and does not recover. Remedies, in the order they usually work: move processing off the thread that drains the stream, narrow the filter, apply accounts_data_slice to cut payload size, read faster.

Resuming

There is no server-side cursor. Consumption is not tracked, so a reconnect starts live unless the request asks for something else. Resumption is the client’s decision, through from_slot: track the last slot processed, set from_slot to it on reconnect, and the stream replays forward and tails into live on the same connection. A client that does not need gap-free delivery reconnects with no from_slot. That path is cheaper and cannot fail on a retention boundary. from_slot resumption suits state reconciliation that genuinely cannot tolerate a hole.

Errors and retries

Rejections carry an HTTP status, a gRPC status code, an x-error-code header and a grpc-message. x-error-code is stable and unique per condition and is the field to branch on. The prose is not stable and is better displayed than matched. grpc-message is always <message> (code: <X_ERROR_CODE>). UNAUTHENTICATED covers a missing token, an invalid one, an expired one and a disallowed source address under one code, so a report of it is checked against the IP allow-list as well as against the credential. AUTH_RATE_LIMITED counts distinct invalid credentials from one address rather than failed attempts. A client stuck in a reconnect loop with one stale or revoked token retries it indefinitely without being blocked, and keeps receiving UNAUTHENTICATED. It is a configuration error to surface rather than a transient to retry. Conditions raised on the streaming path carry a status and a message but no x-error-code: Both OUT_OF_RANGE wordings name the oldest retained slot, so parsing for retained from is more reliable than matching either string whole.

Backoff

Statuses 13 and 14 retry with exponential backoff starting at 100 ms, capped at 30 seconds, with jitter. That covers AUTH_UNAVAILABLE, AUTH_BUSY, NO_CAPACITY, UPSTREAMS_UNAVAILABLE and UPSTREAM_ERROR. Status 8 carrying the replay capacity message retries the same way, as does a bare connection failure. Retrying 16 or 3 without changing the credential or the request repeats the same failure. On 11, the available window is read out of the message and the subscription resumes from there.