Yellowstone gRPC filters in practice, and the same filter set over a WebSocket bridge
A Yellowstone subscription is one request describing everything you want. How its fields combine, what happens when you change it, and how to split a workload across a fixed number of streams.
Yellowstone (also called Dragon's Mouth) is the gRPC interface most Solana backends use to stream accounts, transactions, slots and blocks. Its filter model is small — a handful of fields per update kind — but the way those fields combine, and the rule that a new request replaces the old one, cause most of the confusing behavior people hit in production.
Filter-logic guides exist; the Subglow write-up on why a SubscribeRequest isn't matching explains AND/OR combination well (read 2026-09-22). It does not cover what happens when you change filters on a live stream, how many streams a plan gives you, or a WebSocket alternative. Those are the design questions this post answers, using the filter semantics from the open-source Yellowstone repository and the limits of our own endpoint.
The five things you can subscribe to
A SubscribeRequest is a set of maps, one per update kind. Each map goes from a name you choose to a filter:
| Update kind | What the filter can say | Empty filter means |
|---|---|---|
accounts | account (pubkeys), owner (program ids), filters (dataSize / memcmp, the same shapes as getProgramAccounts) | every account update |
transactions | vote, failed, signature, accountInclude, accountExclude, accountRequired | every transaction |
slots | filterByCommitment | slot updates at every commitment level |
blocks / blocksMeta | accountInclude plus include* switches for transactions, accounts and entries; block meta has no filter | every block |
entry | no filter fields | every entry |
The request also carries request-wide options: commitment (processed, confirmed or finalized; see commitment level), accountsDataSlice to trim account data server-side, and ping for keepalive.
The "empty filter" column is the first trap. An account filter with no fields is not a no-op; it asks for the entire account firehose. The repository says it plainly: "If all fields are empty, then all accounts are broadcast."
How fields combine: AND across fields, OR within arrays
The repository states the rule for accounts: "fields work as logical AND and values in arrays as logical OR (except values in filters that works as logical AND)". Transactions follow the same rule, and the three account lists give you all three set operations:
accountInclude— the transaction touches any of these;accountRequired— it touches all of these;accountExclude— it touches none of these.
So this filter:
{
"transactions": {
"swaps": {
"vote": false,
"failed": false,
"accountInclude": ["PROGRAM_A", "PROGRAM_B"],
"accountRequired": ["POOL_X"],
"accountExclude": ["NOISY_ACCOUNT"]
}
},
"commitment": "confirmed"
}means: not a vote, not failed, AND touches program A or program B, AND touches pool X, AND does not touch the noisy account.
For accounts, account and owner are ANDed when both are set, which surprises people who expect "these accounts, or anything this program owns". If you want that union, use two named filters — which is the next rule.
Named filters are alternatives, and they come back to you
Each map can hold several named filters. An update is delivered when it matches any of them, and the filters field of every update lists the names that produced it. On our endpoint the reference describes that field as "filter names (your SubscribeRequest map keys) that produced this update".
This is what makes one stream serve several consumers. Name filters after the consumer that owns them — wallets-hot, pool-watch, oracle-accounts — and route each update by its filters list instead of re-matching accounts in your code. The list can hold more than one name when an update matches several filters, so a router that fans out by name must expect that.
Replace, not merge: every request swaps the whole set
The rule that matters most in production is the least visible one. On the Yellowstone Subscribe stream, each new SubscribeRequest replaces the previously active filter set as a whole: there is no incremental add/remove and no per-filter unsubscribe.
Three consequences:
- Keep the full set in your client. To add one wallet, you resend every filter with the new wallet included. A client that sends "just the new filter" silently drops everything else.
- Resend the whole set after every reconnect. Filter state is not kept on the server between connections. The docs describe the recovery step as reconnecting and re-sending the
SubscribeRequest. - An empty block unsubscribes that kind. Sending
"transactions": {}in a later request stops transaction updates — useful on purpose, painful by accident.
Treat the filter set as data you own: a versioned object in your service, built from your watch-list, serialized whole on every change and on every connect.
Designing within stream caps
On Triport, Yellowstone gRPC access starts on Pro and is limited by concurrent streams, not requests per second: 8 on Pro and 20 on Business, as listed on the pricing page. Opening a stream beyond the cap is refused; on the gRPC endpoint the troubleshooting table lists RESOURCE_EXHAUSTED for "the key reached its concurrent stream cap".
That makes the design question "how many streams, holding which filters?". A few rules of thumb:
- One stream per consumer group, not per target. A thousand watched wallets is one
accountsfilter with a thousand pubkeys, not a thousand streams. (The open-source server lets an operator cap the number of filters and of pubkeys per filter, so split a very large list across several named filters rather than assuming one array can grow forever.) - Separate streams by volume. Put the high-volume filter (a busy program's transactions) on its own stream, so a slow consumer on that stream does not starve the quiet ones. The docs note that a consumer that cannot keep up is buffered to a bound and then disconnected.
- Keep a spare. Reserve one stream for the replacement you open during a deploy or a reconnect, so you never have to close a healthy stream to open a new one.
- Reconnect with backoff. A client that reconnects in a tight loop churns streams without receiving anything useful; back off exponentially and keep streams long-lived.
- Trim data server-side.
accountsDataSlicereturns only the byte ranges you parse.
Filters are cheap and streams are not, so push complexity into filters. The limit you should expect to feel first is bandwidth into your consumer, which is why narrow filters matter more than clever ones.
gRPC vs. /ws/sol-stream: not the same filter model
/ws/sol-stream is not a WebSocket bridge for this filter model. Measured on 2026-09-22 and described in the /ws/sol-stream reference, it pushes one fixed feed of non-vote transactions (slot, signature, fee) to every client and ignores client messages, so none of the filters above apply there. The tier gate is the same Yellowstone category, starting on Pro.
gRPC (geyser.Geyser/Subscribe) | /ws/sol-stream | |
|---|---|---|
| Client | Generated protobuf stubs or a Yellowstone client library | Any WebSocket client, JSON frames |
| Endpoint | triport.io:443, key in x-token metadata | wss://triport.io/ws/sol-stream, key in a header or ?api-key= |
| Filter model | SubscribeRequest, whole-set replacement | None: one fixed non-vote transaction feed for every client |
| Keepalive | ping in a request | Server WebSocket ping every 30 s |
| Good fit | Anything that needs the filters above | Watching the overall non-vote transaction flow |
Pick gRPC whenever you need the filters above; /ws/sol-stream fits only when the whole non-vote flow is what you want. For a lower-volume alternative with one subscription per account, see the JSON-RPC pub/sub channel in the Solana WebSocket reference, and for the trade-off between the three transports in general, see SSE vs WebSocket vs gRPC.
At-most-once delivery and the gap after a reconnect
The stream is live, not durable. Our docs describe delivery as "at-most-once push — no replay or backfill of frames produced before you connected". Whatever happened while you were disconnected is not sent later. The recovery pattern is the same one every stream needs:
- Record the last slot you fully processed.
- On reconnect, resend the full filter set.
- Backfill the gap over JSON-RPC — the docs name
getBlockandgetSignaturesForAddress— then resume from the stream. - Deduplicate: frames at the boundary can arrive both from the backfill and the new stream.
Commitment choice interacts with this. processed updates arrive first and can still be rolled back; finalized is irreversible. A consumer that acts on money should either subscribe at confirmed or later, or re-check what it acted on. Our finality post compares these levels with other chains.
When this is the wrong approach
- A handful of accounts. For five wallets,
accountSubscribeon the JSON-RPC WebSocket is simpler and does not need Pro. - History. No filter reaches into the past. Read history with
getSignaturesForAddressandgetTransaction, then stream from now. - Browser clients. Yellowstone streams can be high-bandwidth and need a key; run them in your backend and push the results to browsers yourself.
- Pre-execution data. A
transactionsfilter sees executed transactions with their results. Streams of transactions before execution are a different product with different fields, and not what this filter model delivers.
Sources
- Yellowstone gRPC repository, README "Filters for streamed data" (AND/OR rules, empty-filter broadcast,
accountInclude/accountExclude/accountRequired, blocks and entries) — https://github.com/rpcpool/yellowstone-grpc, read 2026-09-22. - Triport, Yellowstone gRPC reference:
geyser.Geyserservice,triport.io:443,x-token, Pro and above,RESOURCE_EXHAUSTEDon the stream cap, no replay buffer. - Triport,
/ws/sol-streamreference: fixed non-vote transaction feed, no client filters, frame fields, at-most-once delivery, reconnect and backfill. - Triport, streaming overview: channel table and per-method tiers. Plan registry: Yellowstone gRPC minimum tier Pro, 8 streams on Pro and 20 on Business. The gRPC endpoint and
/ws/sol-streamwere checked on production on 2026-09-22. - Subglow, "Yellowstone gRPC Filters: Why Your SubscribeRequest Isn't Matching" — https://subglow.io/yellowstone-grpc-filters, read 2026-09-22 (fields ANDed, array values ORed, multiple named filters ORed; filter replacement, stream caps and a WebSocket alternative not found).