Solana High-Throughput Stream — /ws/sol-stream
A single bidirectional WebSocket that carries the full Solana firehose — accounts, transactions, slots, blocks, and entries — driven by one replaceable filter set.
/ws/sol-stream is the high-throughput streaming surface for Solana. Unlike
/ws/sol (JSON-RPC pub/sub with one subscription id per
call), this channel carries a single, comprehensive filter set over one
connection: you describe everything you want — which accounts, which
transactions, which slots/blocks/entries — in one SubscribeRequest, and the
server pushes back a stream of SubscribeUpdate frames.
Each SubscribeRequest atomically replaces the previously active filter
set. There is no incremental add/remove and no per-filter unsubscribe: to
change what you receive, send a new full request. An empty filter block (e.g.
"transactions": {}) effectively unsubscribes that update kind.
Use this channel when you need volume and low latency across many account or
transaction targets at once — indexers, mempool/landed-tx watchers,
block followers. For a handful of individual account or program subscriptions,
prefer the simpler JSON-RPC /ws/sol channel.
This is a pro-tier-and-up surface. Access is gated by the number of
concurrent streams (open connections) your tier allows — max_streams — rather
than by a requests-per-second bucket. There is no RPS limit and no daily
quota on this channel.
Parameters
A SubscribeRequest payload is a JSON object whose top-level keys are filter
blocks. Each named filter block (accounts, transactions, …) is a
map from a caller-chosen filter name to a filter shape; the filter name is
echoed back in each update's filters array so you can tell which filter
produced a frame.
SubscribeRequest payloadobjectaccountsmap<string, AccountsFilter>optionalslotsmap<string, SlotsFilter>optionaltransactionsmap<string, TransactionsFilter>optionaltransactionsStatusmap<string, TransactionsFilter>optionalblocksmap<string, BlocksFilter>optionalblocksMetamap<string, object>optionalentrymap<string, object>optionalcommitmentstringoptionalprocessed, confirmed, finalized. Default confirmed.accountsDataSlicearray<{offset, length}>optionaldata to reduce payload size. Each entry needs offset ≥ 0 and length ≥ 0.ping{id: int32}optionalResponse
The server pushes SubscribeUpdate frames. Each frame populates exactly one
of the 8 update keys (account, slot, transaction, transactionStatus,
block, blockMeta, entry, pong) plus the envelope fields filters and
createdAt. Narrow on key presence.
A transaction update:
An account update (with accountsDataSlice applied, so data is truncated):
{
"account": {
"slot": 295310490,
"account": {
"pubkey": "4Nd1m...A1bc",
"owner": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA",
"lamports": 2039280,
"executable": false,
"rentEpoch": 361,
"data": "AQAAAA==",
"writeVersion": 184467440737,
"txnSignature": "5j7s...n2Qd"
}
},
"filters": ["client_filter_1"],
"createdAt": "2026-05-29T12:34:57.011Z"
}filtersstring[]SubscribeRequest map keys) that produced this update.createdAtstring (date-time)accountUpdateAccountslot + account (pubkey, owner, lamports, executable, rentEpoch, data base64, writeVersion, txnSignature).slotUpdateSlotslot, parent, status (processed/confirmed/finalized), deadError.transactionUpdateTransactionslot + transaction (signature, isVote, full transaction envelope, meta).transactionStatusUpdateTransactionStatusslot, signature, isVote, index, err.blockUpdateBlockslot, blockhash, parentSlot, blockTime, blockHeight, executedTransactionCount, plus optional transactions/accounts/entries/rewards.blockMetaUpdateBlockMetablock without the embedded arrays.entryUpdateEntryslot, index, numHashes, hash, executedTransactionCount.pong{id: int32}Keepalive
To keep an idle connection alive, send a ping — either as a standalone Ping
message or inline via the ping field of a SubscribeRequest:
{ "ping": { "id": 42 } }The server replies with a pong update echoing the same id:
{ "pong": { "id": 42 }, "filters": [], "createdAt": "2026-05-29T12:35:10.000Z" }Errors
Before closing, the server may send a WSErrorFrame (shared error envelope),
then close with one of the canonical WebSocket close codes. The RFC-6455 close
code is on the close frame itself, not in the JSON body.
| Close code | error | When it happens |
|---|---|---|
4001 | unauthorized | Missing or invalid Authorization / api-key. |
4003 | forbidden | Tier too low (below pro), or you exceeded your max_streams concurrent-connection cap. |
4029 | rate_limited | Connection-rate / abuse protection tripped. |
4030 | trial_expired | Trial period ended; upgrade required. |
Example error frame preceding a 4003 close:
{
"error": "forbidden",
"message": "tier 'basic' cannot open sol-stream; requires pro",
"current_tier": "basic",
"required_tier": "pro",
"category": "sol_yellowstone",
"upgrade_url": "https://triport.io/upgrade?tier=pro"
}See the shared errors page for the full WSErrorFrame envelope
and SDK error-class mapping.
Examples
JavaScript (fetch / ws)
import WebSocket from "ws";
const ws = new WebSocket("wss://ws.triport.io/ws/sol-stream", {
headers: { Authorization: `Bearer ${process.env.TRIPORT_API_KEY}` },
});
ws.on("open", () => {
ws.send(JSON.stringify({
transactions: {
client_filter_1: {
vote: false,
failed: false,
accountInclude: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
},
},
commitment: "confirmed",
}));
});
ws.on("message", (raw) => {
const u = JSON.parse(raw);
if (u.transaction) console.log("tx", u.transaction.transaction.signature, "@ slot", u.transaction.slot);
else if (u.pong) console.log("pong", u.pong.id);
});TypeScript SDK (@triport/sdk)
import { Triport } from "@triport/sdk";
const client = new Triport({ apiKey: process.env.TRIPORT_API_KEY! });
const stream = client.solana.stream({
transactions: {
client_filter_1: {
vote: false,
failed: false,
accountInclude: ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
},
},
commitment: "confirmed",
});
for await (const update of stream) {
if (update.transaction) {
console.log(update.transaction.transaction.signature, update.transaction.slot);
}
}
// Atomically replace the filter set — start watching slots instead:
await stream.update({ slots: { my_slots: { filterByCommitment: true } }, commitment: "finalized" });Python (triport-sdk)
import os
from triport import Triport
client = Triport(api_key=os.environ["TRIPORT_API_KEY"])
stream = client.solana.stream(
transactions={
"client_filter_1": {
"vote": False,
"failed": False,
"accountInclude": ["TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"],
}
},
commitment="confirmed",
)
for update in stream:
if "transaction" in update:
tx = update["transaction"]
print(tx["transaction"]["signature"], tx["slot"])Notes
- Atomic filter replacement. Each
SubscribeRequestfully replaces the previous filter set. There is no per-filter unsubscribe — send a new request with the desired blocks, and use empty blocks ({}) to drop an update kind. max_streams, not RPS. Your tier caps the number of concurrent open/ws/sol-streamconnections (pro = 8, business = 20). Opening one beyond your cap closes the new connection with4003 forbidden. There is no per-message RPS bucket and no daily quota on this channel.accountsDataSlicetrims thedatafield of account updates server-side — setoffset/lengthto fetch only the bytes you parse, which can sharply cut bandwidth on large accounts.- Pick the right channel. For low-volume, individually-managed
subscriptions,
/ws/solis simpler. For Ethereum/Polygon streams see/ws/ethand/ws/polygon. For curated account-change pushes see/ws/sol-watch. - Commitment defaults to
confirmed; chooseprocessedfor lowest latency (may revert) orfinalizedfor irreversibility.