> 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/fumarole-persistent-streams.md).

# Fumarole Persistent gRPC

How Fumarole persistent gRPC streams resume from a server-side cursor with 4-day replay, using the Fume CLI or Rust and TypeScript clients.

Fumarole is reliable, persistent gRPC streaming for pipelines that can't miss a block: disconnect and resume from your last committed position, with anything you missed backfilled automatically from a 4-day rolling cache.

## Use cases

Fumarole fits pipelines that value completeness and reliability over raw speed:

* **Indexers** that must process every block.
* **Analytics and accounting** systems that cannot tolerate data gaps.
* **Compliance and monitoring** tools that need a complete record.

Don't use it for latency-first workloads. If you need data as fast as possible for trading, look into [Dragon's Mouth gRPC](/solana/real-time-streaming/dragon-s-mouth-grpc.md) instead.

## Features and benefits

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-arrows-rotate">:arrows-rotate:</i> <strong>High availability</strong></td><td>Multi-node aggregation with automatic failover. A Dragon's Mouth node restart or upgrade doesn't interrupt your stream.</td><td></td></tr><tr><td><i class="fa-database">:database:</i> <strong>Persistence</strong></td><td>Server-side cursor with 4 days of rolling retention. Go offline for hours or days and resume from your last committed position.</td><td></td></tr><tr><td><i class="fa-circle-check">:circle-check:</i> <strong>At-least-once delivery</strong></td><td>Whole blocks are served, with full block data arriving before slot status. A reconnect backfills what you missed.</td><td></td></tr><tr><td><i class="fa-equals">:equals:</i> <strong>Consistent data</strong></td><td>Inside a region, clients with the same filters and commitment level get the same data for a slot, and blocks are scheduled for download in the same order. Network differences can complete downloads in a different order, so after a reconnect you may not receive events in the same order.</td><td></td></tr><tr><td><i class="fa-layer-group">:layer-group:</i> <strong>Parallel downloads</strong></td><td>The Rust client downloads block data over several gRPC connections at once (4 by default), so one slow transfer doesn't hold up the rest.</td><td></td></tr><tr><td><i class="fa-code-fork">:code-fork:</i> <strong>Dragon's Mouth compatible</strong></td><td>Fumarole uses the same types as Dragon's Mouth. Fumarole ignores some <code>SubscribeRequest</code> fields, including the <code>blocks</code> filter, <code>accounts_data_slice</code>, <code>from_slot</code>, <code>interslot_updates</code>, and compressed filters.</td><td></td></tr></tbody></table>

## How it works

1. Fumarole aggregates multiple Dragon's Mouth nodes, merging and deduplicating their updates into one stream.
2. It persists your read position as a server-side cursor, with 4 days of rolling retention.
3. You stream from a named persistent subscriber at your own pace. If you disconnect, reconnecting with the same name resumes from your last committed position, anywhere in the 4-day window.

```mermaid
%%{init: {'theme':'base','themeVariables':{'primaryColor':'#F2EDF6','primaryBorderColor':'#7A4BA0','primaryTextColor':'#171717','lineColor':'#956FB3','secondaryColor':'#E4DBEC','tertiaryColor':'#D7C9E3','edgeLabelBackground':'#F2EDF6'},'flowchart':{'nodeSpacing':20,'rankSpacing':35,'curve':'linear'}}}%%
flowchart LR
    n1["Dragon's Mouth"] --> f
    n2["Dragon's Mouth"] --> f
    n3["Dragon's Mouth"] --> f["Fumarole<br/>merge + dedupe + cursor<br/>(4-day retention)"]
    f --> you["Your app"]
    style you fill:#D6EAF8,stroke:#259DD0
```

## Client methods

Don't call Fumarole's gRPC methods directly. Use the Rust client, the TypeScript client, or the Fume CLI, which call them for you. The client APIs call a persistent subscriber a consumer group.

| Task                                                     | Rust client                                                                | TypeScript client                                                   |
| -------------------------------------------------------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Create a persistent subscriber                           | `create_consumer_group`                                                    | `createConsumerGroup`                                               |
| List, inspect, and delete persistent subscribers         | `list_consumer_groups`, `get_consumer_group_info`, `delete_consumer_group` | `listConsumerGroups`, `getConsumerGroupInfo`, `deleteConsumerGroup` |
| Stream from a persistent subscriber                      | `subscribe`, `subscribe_with_config`                                       | `subscribe`, `subscribeWithConfig`                                  |
| Read the stream's tip, available slot range, and version | `get_chain_tip`, `get_slot_range`, `version`                               | `getChainTip`, `getSlotRange`, `version`                            |

## Get started

Fumarole works with your existing mainnet subscription token, no separate access request needed.

**1. Pick a regional endpoint.** Connect directly to the cluster closest to your backend (see [Regional endpoints](#regional-endpoints)): `ams.rpcpool.com` (EU) or `nyc.rpcpool.com` (US). Persistent subscribers are stateful per cluster, so avoid the shared `*.mainnet.rpcpool.com` endpoints.

**2. Authenticate with your token.** Pass your existing mainnet token as the `x-token`.

**3. Install a client.** Use the Fume CLI to try Fumarole and to create a persistent subscriber; the Rust and TypeScript SDKs are also available.

{% tabs %}
{% tab title="Fume CLI" %}

```bash
cargo install yellowstone-fumarole-cli
```

Save your endpoint and token in `~/.fumarole/config.yaml`:

```yaml
endpoint: https://ams.rpcpool.com
x-token: <your-token>
```

Create the persistent subscriber with the `fume` command. It starts at the latest slot.

```bash
fume create --name my-subscriber
```

{% endtab %}

{% tab title="Rust" %}

```toml
[dependencies]
yellowstone-fumarole-client = "0.9"
yellowstone-grpc-proto = "14"
serde_yaml = "0.9"
tokio = { version = "1", features = ["full"] }
futures = "0.3"
```

{% endtab %}

{% tab title="TypeScript" %}
The TypeScript client requires Node.js 22 or later.

```bash
npm install @triton-one/yellowstone-fumarole-client
```

{% endtab %}
{% endtabs %}

**4. Create a persistent subscriber, then stream from it.** The subscriber tracks your position server-side, so reconnecting with the same name resumes from your last committed position.

{% tabs %}
{% tab title="Rust" %}

```rust
use futures::StreamExt;
use std::collections::HashMap;
use yellowstone_fumarole_client::{FumaroleClient, config::FumaroleConfig, stream::FumaroleEvent};
use yellowstone_grpc_proto::geyser::{
    SubscribeRequest, SubscribeRequestFilterSlots, SubscribeRequestFilterTransactions,
};

#[tokio::main]
async fn main() {
    let config: FumaroleConfig = serde_yaml::from_str(
        "endpoint: https://ams.rpcpool.com\nx-token: <your-token>",
    )
    .expect("valid config");

    let mut client = FumaroleClient::connect(config).await.expect("connect");

    // No commitment is set, so the stream runs at processed.
    // The slot filter delivers the confirmed and finalized statuses as slot updates.
    let request = SubscribeRequest {
        transactions: HashMap::from([(
            "f1".to_owned(),
            SubscribeRequestFilterTransactions::default(),
        )]),
        slots: HashMap::from([("s1".to_owned(), SubscribeRequestFilterSlots::default())]),
        ..Default::default()
    };

    // "my-subscriber" is a persistent subscriber you created (e.g. with the Fume CLI).
    let subscription = client
        .subscribe("my-subscriber".to_string(), request)
        .await
        .expect("subscribe failed");

    let (_sink, stream) = subscription.split();
    let mut slot_stream = stream.slot_sequential();

    while let Some(item) = slot_stream.next().await {
        match item.expect("stream error") {
            FumaroleEvent::Data { slot, update, .. } => println!("slot {slot}: {update:?}"),
            FumaroleEvent::SlotEnded { slot, .. } => println!("slot {slot} complete"),
        }
    }
}
```

{% endtab %}

{% tab title="TypeScript" %}

```typescript
import {
  FumaroleClient,
  SubscribeRequest,
  CommitmentLevel,
} from "@triton-one/yellowstone-fumarole-client";

const client = await FumaroleClient.connect({
  endpoint: "https://ams.rpcpool.com",
  xToken: "<your-token>",
});

// Create the persistent subscriber once; reuse the name to resume.
const subscriberName = "my-subscriber";
if ((await client.getConsumerGroupInfo(subscriberName)) === null) {
  await client.createConsumerGroup(subscriberName);
}

// Stream at processed. The slot filter delivers the confirmed and finalized statuses as slot updates.
const request: SubscribeRequest = {
  commitment: CommitmentLevel.PROCESSED,
  accounts: {},
  transactions: { f1: { accountInclude: [], accountExclude: [], accountRequired: [] } },
  slots: { s1: {} },
  transactionsStatus: {},
  blocks: {},
  blocksMeta: {},
  entry: {},
  blockFooter: {},
  accountsDataSlice: [],
};

const subscription = await client.subscribeWithConfig(subscriberName, request, {
  commitIntervalMs: 5_000,
});

for await (const event of subscription) {
  if (event.type === "slotEnded") {
    console.log(`slot ${event.slot} complete`);
  } else {
    console.log(`slot ${event.slot}:`, event.update);
  }
}
```

{% endtab %}
{% endtabs %}

**Commitment level.** Stream at `processed` and track `confirmed` and `finalized` in your own code from the slot updates. If you set a higher `commitment` on the request, the client waits for a block to reach that level before it downloads the block.

**Block footers.** Add a `block_footer` filter to the request to receive Alpenglow block footers. A footer arrives once per block, and only where Alpenglow is active. On the raw stream it comes right before the block's meta update; in slot-sequential mode the meta update is delivered immediately and can arrive before the footer. Set `include_certificates: true` on the filter to also receive the certificates. The footer's `bank_id` is set by Fumarole per block, so it joins the block's other updates; it is not the validator's `bank_id`. In the Fume CLI, `--include footer` prints footers and `--footer-certs` adds the certificates (`yellowstone-fumarole-cli` 0.8.0 or later).

For the full Fume CLI walkthrough, read the [launch post](https://blog.triton.one/introducing-yellowstone-fumarole); for the complete API, see the [Rust and TypeScript SDKs](https://github.com/rpcpool/yellowstone-fumarole).

## Streaming modes

The Rust client streams in three modes:

* **Slot-sequential.** Data updates one slot at a time. Slot status and block meta updates pass through as they arrive, even while another slot is active.
* **Block stream.** Whole blocks as single payloads, for logic that processes entire blocks at once.
* **Raw stream.** Events as the client runtime produces them, with no slot grouping or reordering.

Any mode can be paired with manual cursor commits, and all three use parallel shard downloads. Rust is the primary client; the TypeScript and Python clients don't support all three modes yet.

By default the Rust client commits your cursor automatically. Set `auto_commit: false` to decide exactly when the cursor moves: write to your database first, then commit receipt after the write succeeds.

## Migrating from Dragon's Mouth

If you already have code built for our gRPC streams in Dragon's Mouth, you can move to Fumarole for additional reliability with minimal code changes, because Fumarole uses the same types as Dragon's Mouth.

The main difference is that you need to manage a **persistent subscriber**, and alter your subscribe request slightly.

For the full walkthrough, follow the [Dragon's Mouth to Fumarole migration guide](/solana/quickstart/streaming/migrate-from-dragons-mouth-to-fumarole.md). For the complete API, see the [yellowstone-fumarole-client docs](https://docs.rs/yellowstone-fumarole-client/latest/yellowstone_fumarole_client/).

## Regional endpoints

Fumarole runs as **independent regional clusters**. We currently operate:

| Region | Endpoint          | Location  |
| ------ | ----------------- | --------- |
| EU     | `ams.rpcpool.com` | Amsterdam |
| US     | `nyc.rpcpool.com` | New York  |

### Choosing an endpoint

For Fumarole, connect directly to a **regional endpoint** rather than shared `*.mainnet.rpcpool.com` endpoints. Pick the region closest to your backend infrastructure to minimize latency.

{% hint style="warning" %}
Shared `*.mainnet.rpcpool.com` endpoints are **not recommended for Fumarole**. See below for why.
{% endhint %}

### Why direct regional endpoints matter

The shared endpoints (`*.mainnet.rpcpool.com`) route traffic to the nearest Triton point of presence over Anycast. For stateless RPC calls this is ideal: any region can serve any request.

### Persistent subscribers are stateful per cluster

A persistent subscriber and the slot offsets it tracks live locally on the cluster where it was created and **do not replicate across regions**.

If you connect through a shared endpoint, your traffic can be routed to a region where your persistent subscriber does not exist, for example after a routing change, a network event, or a change in your backend's network path. When that happens, your subscriber will not be found on the new cluster and you will need to recreate it. Pointing your Fumarole client directly at a regional endpoint avoids this entirely.

### Switching regions

To move a persistent subscriber from one regional cluster to another (e.g., EU to US):

{% stepper %}
{% step %}

### Recreate your persistent subscriber

Create it on the new cluster: subscribers do not carry over between regional clusters.
{% endstep %}

{% step %}

### Point your client to the new regional endpoint

{% endstep %}

{% step %}

### Reconnect

Your existing token continues to work; no token changes are required.
{% endstep %}
{% endstepper %}

### Cross-region redundancy (customer-managed)

With multiple regional clusters available, you can implement cross-region redundancy on the client side. If one cluster experiences a major issue, you can fail over to another region by recreating the persistent subscriber **from the last slot you observed on the failing cluster**. Fumarole tracks the last **full** slot you consumed.

This pattern is fully customer-managed:

* Your client tracks the last-seen slot per cluster.
* Your client detects the failure and triggers the cutover.
* Triton does not synchronize subscriber state, track last-seen slots, or orchestrate failover between clusters.

If you need this level of redundancy, plan your slot bookkeeping and failover logic accordingly. For the full step-by-step detection, failover, and failback procedure, follow the [Fumarole cluster failover guide](/solana/quickstart/streaming/fumarole-cluster-failover.md).

## Limits

* Each subscription can have 15 persistent subscribers on each regional cluster.
* Every persistent subscriber counts until you delete it, including a stale one that fell behind the 4-day window.
* Creating a subscriber past the limit fails with `FAILED_PRECONDITION` and the message `Reached consumer groups limit of 15`.
* To free a slot, run `fume list`, then `fume delete --name <name>`.

## Pricing

On shared infrastructure, Fumarole is billed on the data sent. For current rates, see [triton.one/pricing](https://triton.one/pricing).

## What's next

<table data-card-size="large" data-view="cards"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th></tr></thead><tbody><tr><td><i class="fa-tower-broadcast">:tower-broadcast:</i> <strong>Dragon's Mouth gRPC</strong></td><td>Sub-slot real-time updates for accounts, transactions, slots, and blocks via gRPC.</td><td><a href="/solana/real-time-streaming/dragon-s-mouth-grpc.md">Dragon's Mouth gRPC</a></td></tr><tr><td><i class="fa-list-check">:list-check:</i> <strong>Best practices</strong></td><td>Filtering, reconnect, and commitment guidance across the streaming services.</td><td><a href="/solana/real-time-streaming/best-practices.md">Real-time streaming best practices</a></td></tr></tbody></table>

***

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