> For the complete documentation index, see [llms.txt](https://docs.triton.one/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.triton.one/solana/real-time-streaming/best-practices.md).

# Real-time streaming best practices

Choose between Dragon's Mouth, WebSocket, Fumarole and Deshred, then tune distance, filters, connections, commitment and reconnects for Solana streaming.

## Pick the right stream

| Stream                                                                      | Use it for                                                                     | How it works                                                                                                                                                                                                                                                                                                           |
| --------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [Dragon's Mouth (gRPC)](/solana/real-time-streaming/dragon-s-mouth-grpc.md) | Low latency on a backend                                                       | Account and transaction updates are pushed over a persistent connection as the node processes them, giving a head start over a polling client. Stream at `processed` and track `confirmed` and `finalized` yourself (see [Handle commitment client-side](#handle-commitment-client-side)).                             |
| [WebSocket](/solana/real-time-streaming/whirligig-websockets.md)            | Browsers and frontends                                                         | Browsers don't support gRPC, so the WebSocket API serves Dragon's Mouth data over `wss://`. Account updates at `processed` commitment arrive intra-slot. `transactionSubscribe` sends each transaction that matches your filter as its own message. `blockSubscribe` sends whole blocks at `confirmed` or `finalized`. |
| [Fumarole](/solana/real-time-streaming/fumarole-persistent-streams.md)      | Reliability-first pipelines (accounting, analytics, indexing, compliance)      | Adds persistence, redundancy, and 4 days of replay on top of Dragon's Mouth, which trades completeness for latency. Use the Rust or TypeScript client. Don't call Fumarole's gRPC methods directly.                                                                                                                    |
| [Deshred](/solana/real-time-streaming/deshred-transactions.md)              | Transactions before they execute (arbitrage, market making, liquidations, HFT) | Reconstructs transactions from shreds before execution, so no confirmation guarantee; pair it with the `transactions` stream when you need finality.                                                                                                                                                                   |

## Provision the subscriber

* **Run your subscriber as close to a Triton region as you can.** Keep round-trip time (RTT) to the endpoint at or below 50 ms; the table below lists approximate values by distance. See [Endpoints, regions, and routing](/using-your-endpoint/endpoints-regions-routing.md) for regions.
* **Provision 5 Gbps minimum for large subscriptions, 10 Gbps for full-chain** (accounts plus transactions). The full feed can spike to \~1.3 to 1.8 Gbps, and cloud instances are often capped at \~1 Gbps by default.
* **Use the latest `@triton-one/yellowstone-grpc` release for high-volume streams.** Vanilla Node.js is too slow for Solana. Since 5.0.1, the client runs the gRPC engine in Rust through NAPI-as-an-Engine (NaaE). Triton's benchmark measured about 400% more data throughput than the previous pure-JavaScript version ([blog post](https://blog.triton.one/grpc-js-alternative-napi-rust/)). After you create the client, call `client.connect()` and use camelCase channel options. The [Go client](https://github.com/rpcpool/yellowstone-grpc/tree/master/examples/golang) is an example that can lag the latest proto; the Rust client is the production-ready reference.
* **Solve frequent disconnects early and rule out a client-side problem** (a weak or under-provisioned setup) before assuming a server fault. Start with [Streams disconnecting](/solana/faq/error-handling/streams-disconnecting.md). [Learn more](https://blog.triton.one/solana-grpc-streaming-optimisation-and-troubleshooting-2026-guide/).

{% hint style="warning" %}
**Run subscribers on a persistent server (VPS or bare metal), not serverless functions** like AWS Lambda. Serverless functions don't suit streaming connections and can leave many open connections on the server, which can degrade service.
{% endhint %}

## Stay on the tip during high load

When your client cannot keep up, data backs up in buffers, you drift from the tip, and the server drops the connection. Most drift is client-side, and is usually caused by one of these:

<details>

<summary>You are too far from a Triton region</summary>

Distance is the biggest driver of RTT. With adaptive window sizing, the HTTP/2 stack in the Rust and TypeScript clients (hyper) grows the window to at most 16 MiB, which puts the ceiling for full-chain streaming near 16 MiB / 2 Gbps, \~67 ms, so target 50 ms or less. Measure it from the machine that runs your subscriber with `ping <your-endpoint-hostname>`, and run close to a Triton region:

| Scenario                   | Typical distance | Expected RTT  |
| -------------------------- | ---------------- | ------------- |
| Same city or metro         | under 50 km      | 1 to 5 ms     |
| Regional, two data centers | 200 to 500 km    | 5 to 15 ms    |
| Cross-country (US)         | \~4,500 km       | 60 to 80 ms   |
| Transatlantic              | \~6,000 km       | 70 to 100 ms  |
| Transpacific               | \~9,000+ km      | 140 to 200 ms |

</details>

<details>

<summary>You receive data you don't use</summary>

Narrow the subscription: filter precisely and trim account data with `accounts_data_slice`. See [Filters and connections](#filters-and-connections).

</details>

<details>

<summary>You subscribe at <code>confirmed</code> or <code>finalized</code></summary>

The server sends each block's content in one batch after the block reaches that level, which adds latency and makes bandwidth bursty. Stream at `processed` and [track commitment in your client](#handle-commitment-client-side).

</details>

<details>

<summary>The flow-control window is smaller than your bandwidth-delay product (BDP)</summary>

gRPC runs on HTTP/2, which only lets the server send a window's worth of data before it waits for an acknowledgement. The volume you need in flight is the BDP, `bandwidth × RTT` (2 Gbps at 10 ms RTT is \~2.38 MiB). A window below the BDP leaves bandwidth idle. Let the client size the window automatically (adaptive window sizing), which Rust client 13.3.0 and later turn on by default in `from_shared`:

```rust
use yellowstone_grpc_client::GeyserGrpcBuilder;

// Client 13.3.0 and later enable adaptive window sizing in from_shared; this call makes it explicit.
let builder = GeyserGrpcBuilder::from_shared("https://<your-endpoint>")?
    .http2_adaptive_window(true);
```

</details>

<details>

<summary>You process inside the receive loop</summary>

If one thread receives a message, parses it, writes it to a database, and only then reads the next, it fills buffers and disconnects under load. Decouple ingestion from processing: the receive loop should only push each message onto a queue or channel, and a worker pool handles parsing and writes.

</details>

<details>

<summary>Your bandwidth is limited, or you are still far from a region after the steps above</summary>

Zstd compression can help, but use it sparingly: it adds latency and CPU on both ends, and adds load on Triton's servers. To enable it in the Rust client:

```rust
use yellowstone_grpc_proto::tonic::codec::CompressionEncoding;

let builder = GeyserGrpcBuilder::from_shared("https://<your-endpoint>")?
    .accept_compressed(CompressionEncoding::Zstd);
```

</details>

Verify with the `client-linux-amd64` test client (x86-64 Linux; see [Verify your gRPC endpoint](/solana/faq/error-handling/verify-your-grpc-endpoint.md)), using the exact flags and filters you will run in production:

```bash
./client-linux-amd64 \
  --http2-adaptive-window true \
  --commitment processed \
  --endpoint https://<your-endpoint> \
  --x-token <your-token> \
  subscribe --transactions --accounts --stats
```

Add `--compression zstd` only if you use Zstd in production.

{% hint style="success" %}
A ping every \~10 seconds and a total of 60-80 Mbps (full-chain feed) mean you are holding the tip.
{% endhint %}

{% hint style="warning" %}
A ping every 12 seconds or more, or none at all, means you are falling behind, so narrow the subscription or fix the causes above.
{% endhint %}

If the stream still disconnects, go through [Streams disconnecting](/solana/faq/error-handling/streams-disconnecting.md).

## Filters and connections

* **Filter precisely:** subscription fields are logical AND and array values logical OR; include or exclude vote and failed transactions.
* **Expect no vote transactions on Alpenglow clusters.** Blocks contain no vote transactions, so `vote: true` matches nothing and `vote: false` excludes nothing. User activity is unchanged.
* **Trim payloads with `accounts_data_slice`** to receive only the bytes you need.
* **For blocks, exclude transactions or scope to specific accounts**, or use the block-meta subscription when you only need block-processed notifications.
* **Open one connection per Dragon's Mouth, Riptide, or Deshred stream.** A stream is one `subscribe` call; several named filters in one subscribe request are still one stream. With two or more such streams, give each stream its own gRPC connection. Streams on one gRPC connection share one TCP connection and one HTTP/2 flow-control window, so a lost packet or a full window delays all of them, and some setups break because of Triton's load balancer.
* **Modify a stream's subscription in place** by sending a new subscribe request on the same bi-directional stream, rather than opening a new stream for each change.

{% hint style="warning" %}
A new subscribe request entirely overwrites the previous one, so **keep a local register of your full subscription config.**
{% endhint %}

## Handle commitment client-side

* **Stream at `processed` and track `confirmed` and `finalized` yourself,** rather than asking the server to buffer by commitment. A `confirmed` or `finalized` subscription receives a block's account, transaction and entry updates only after the block reaches that level; a `processed` subscription receives them as they arrive.
* **Do the work at `processed` and commit once you observe the commitment.**
* **Buffer events per bank, not per slot.** Every block is produced by a bank, identified by a `bank_id`. A slot usually has one bank, but it can have several competing ones. Key the buffer by `(slot, bank_id)`; a bank ID is unique only within one connection.
* **On a `processed` stream, events for one bank arrive in this order:**
  1. Content, in any order: `CreatedBank` (slot status `SLOT_CREATED_BANK`), entries, transactions, and account updates, including the sysvars `Clock`, `SlotHashes`, `SlotHistory` and `RecentBlockhashes` (`Clock` and `SlotHashes` can arrive before `CreatedBank`).
  2. Block footer: at most one per bank, on Alpenglow clusters only.
  3. `BlockMeta`, which marks the end of the bank's block.
  4. Commitment updates: `Processed`, then `Confirmed`, then `Finalized` (slot statuses `SLOT_PROCESSED`, `SLOT_CONFIRMED`, `SLOT_FINALIZED`).
* **Release a bank's buffered events when the `Confirmed` or `Finalized` update for that `bank_id` arrives,** at the level you need. On a `processed` stream, nothing about a bank's content arrives after its `BlockMeta`, and no commitment update for that bank arrives before it.
* **Subscribe to `slots` and leave `filter_by_commitment` unset** to receive all three commitment updates on a `processed` stream.
* **Set `interslot_updates` on the `slots` filter to also receive `SLOT_CREATED_BANK`.** See [Intra-slot updates](/solana/real-time-streaming/dragon-s-mouth-grpc.md#intra-slot-updates).
* **Expect commitment updates for ancestor slots too.** When a slot reaches `confirmed` or `finalized`, the server also sends that status for each ancestor slot that has not reached it yet.
* **If you use `subscribe_with_reconnect`, drop the state of the banks each `Disconnected` event lists,** and skip banks a replay resends that you already hold whole. See [Reconnect with bank recovery](/solana/real-time-streaming/dragon-s-mouth-grpc.md#reconnect-with-bank-recovery).

## Reconnect and replay

* **Recover from short disconnects with `from_slot`:** track your last processed slot, then reconnect and resubscribe from it. The server replays buffered updates, then continues live.
* **Deduplicate replayed updates** (replay starts at a slot boundary, so the boundary slot may repeat).
* **Check the earliest replayable slot first** via `SubscribeReplayInfo` (`first_available`); if `from_slot` is too old the request fails, so fall back to a fresh live subscription or your own backfill path.
* **Let the client reconnect:** `subscribe_with_reconnect` (Rust, `yellowstone-grpc-client` 17.0.0 and later) and `subscribeWithReconnect()` (TypeScript, `@triton-one/yellowstone-grpc` 10.0.0 and later) reconnect at any commitment level, replay from the last confirmed slot seen by default, and emit a `Disconnected` event listing the partly delivered banks, which your code must drop. Filter changes apply to every reconnect. They don't accept a startup snapshot. Ordinary subscriptions never reconnect. See [Reconnect with bank recovery](/solana/real-time-streaming/dragon-s-mouth-grpc.md#reconnect-with-bank-recovery).

***

<i class="fa-life-ring">:life-ring:</i> Contact support by clicking the chat icon in your [customer dashboard](https://customers.triton.one)\ <i class="fa-briefcase">:briefcase:</i> Sales questions? [Contact us](https://triton.one/contact)\ <i class="fa-sparkles">:sparkles:</i> AI agent? Read [llms.txt](https://docs.triton.one/llms.txt)\ <i class="fa-rss">:rss:</i> Follow updates: [Blog](https://blog.triton.one) · [X](https://x.com/triton_one) · [YouTube](https://www.youtube.com/@triton_one_ltd) · [Telegram](https://t.me/tritonone) · [GitHub](https://github.com/rpcpool)
