All articles
Infrastructure·October 25, 2025·5 min read

Yellowstone gRPC Streaming on Solana: Complete Setup Guide

Step-by-step walkthrough for connecting to a Yellowstone-compatible geyser plugin via gRPC to stream accounts, transactions, and slots in real time. Covers authentication, filtering by program ID, and handling backpressure in production.

If you are building latency-sensitive systems on Solana — MEV bots, liquidity managers, on-chain order matching — polling RPC nodes is not a real option. The Yellowstone geyser plugin exposes a gRPC interface that pushes account updates, transactions, and slot notifications to your client the moment the validator processes them, cutting your observation latency to single-digit milliseconds in practice. This guide walks through everything you need to go from zero to a production-grade streaming client, including the parts the official docs gloss over.

What Yellowstone Actually Is

Yellowstone is a geyser plugin maintained by Triton One that implements the GeyserPlugin interface Solana validators expose for data extraction. The plugin opens a gRPC server — built on tonic in Rust — that speaks a Protobuf-defined protocol described in yellowstone-grpc-proto. Clients subscribe with a SubscribeRequest message specifying which data streams they want: accounts, transactions, slots, blocks, or block metadata. The server pushes SubscribeUpdate messages back over a bidirectional streaming RPC. The bidirectional part matters — you can send additional filter updates on the same stream without reconnecting, which is critical for trading bots that need to track dynamically discovered program accounts.

The proto definitions live in the yellowstone-grpc-proto crate. Pull them with:

cargo add yellowstone-grpc-proto yellowstone-grpc-client tokio tonic

For non-Rust clients, generate stubs from the .proto files directly. The TypeScript ecosystem has @triton-one/yellowstone-grpc which wraps this cleanly.

Authentication and Connection

Every public Yellowstone endpoint requires a token passed as gRPC metadata. The header key is x-token. Using tonic:

use tonic::metadata::MetadataValue;
use yellowstone_grpc_client::GeyserGrpcClient;

let token: MetadataValue<_> = "YOUR_TOKEN_HERE".parse().unwrap();
let mut client = GeyserGrpcClient::build_from_shared(endpoint)
    .x_token(Some(token))?
    .connect()
    .await?;

Use TLS. Triton's endpoints enforce it; if you are running your own Yellowstone node behind a reverse proxy, terminate TLS at the proxy and pass the token through. Keep your token out of source control — it authenticates at the provider level and a leaked token gets rate-limited or banned on the shared infrastructure.

Connection timeouts deserve explicit configuration. The default tonic channel has no connect timeout, meaning a hung TCP handshake blocks indefinitely. Set connect_timeout to 10 seconds and timeout (per-RPC) to something sensible for your use case — 30 seconds works for persistent streams because gRPC keepalives handle the heartbeat separately.

Building a Useful Subscribe Request

The SubscribeRequest is where most engineers waste time. Over-broad filters hammer your processing pipeline; under-broad filters miss events. For transaction monitoring on a specific program:

use std::collections::HashMap;
use yellowstone_grpc_proto::prelude::*;

let mut transactions = HashMap::new();
transactions.insert(
    "my_filter".to_string(),
    SubscribeRequestFilterTransactions {
        vote: Some(false),
        failed: Some(false),
        account_include: vec!["YOUR_PROGRAM_ID".to_string()],
        account_exclude: vec![],
        account_required: vec![],
        ..Default::default()
    },
);

let request = SubscribeRequest {
    transactions,
    commitment: Some(CommitmentLevel::Confirmed as i32),
    ..Default::default()
};

A few non-obvious points here:

  • vote: Some(false) is essential. Vote transactions make up roughly 80% of Solana slot traffic. Including them floods your consumer with noise.
  • commitment: Confirmed gives you a good latency-safety balance. Processed is faster but you will see transactions that roll back. Finalized adds ~32 slots of lag — avoid it for anything latency-sensitive.
  • account_include filters to transactions where any account in the message matches your program ID. It does not guarantee the program was invoked; check the transaction.message.instructions yourself.

For account subscriptions (watching a specific account's lamport balance or data), use SubscribeRequestFilterAccounts with the account field set to a list of base58-encoded pubkeys.

Consuming the Stream and Handling Backpressure

The gRPC stream returns a Streaming<SubscribeUpdate> from tonic. The naive pattern — while let Some(msg) = stream.next().await — works in development but breaks under load. gRPC's flow control will throttle the server if your consumer falls behind, and with Solana processing 3,000–5,000 non-vote TPS at peak, falling behind is easy.

The production pattern: receive on one async task, push into a bounded channel, process on one or more worker tasks.

let (tx, mut rx) = tokio::sync::mpsc::channel::<SubscribeUpdate>(10_000);

tokio::spawn(async move {
    while let Some(Ok(update)) = stream.next().await {
        if tx.send(update).await.is_err() {
            break; // receiver dropped
        }
    }
});

// worker
while let Some(update) = rx.recv().await {
    handle_update(update).await;
}

Size the channel based on your burst profile. A channel of 10,000 gives you roughly 2–3 seconds of buffer at peak TPS. If tx.send returns an error because the channel is full, you have a processing bottleneck — add workers or optimize the hot path before increasing the buffer. Increasing the buffer without fixing throughput just delays the failure.

Reconnection Logic

Yellowstone connections drop. Validators restart, network partitions happen, provider maintenance occurs. Your client must reconnect automatically and resume at the correct state. There is no server-side cursor — you are responsible for tracking what you last processed.

A minimal reconnect loop:

loop {
    match connect_and_stream(&config).await {
        Ok(()) => {} // stream closed cleanly
        Err(e) => {
            eprintln!("stream error: {e}");
            tokio::time::sleep(Duration::from_secs(2)).await;
        }
    }
}

Implement exponential backoff with a cap — something like 2s, 4s, 8s, max 30s. Log the disconnect with a timestamp and the last slot you observed; this is your audit trail when you need to explain a gap in data to stakeholders.

Running Your Own Node vs. Using a Provider

Running a Yellowstone-enabled validator yourself costs roughly $800–$1,200/month in bare-metal hardware (NVMe RAID, 512 GB RAM, high-core Epyc or Threadripper) plus colocation, and you need operator expertise to keep it voting cleanly. The upside is zero token cost, guaranteed bandwidth, and the ability to run at Processed commitment with confidence. Triton One, Helius, and QuikNode all offer managed Yellowstone endpoints at $50–$500/month depending on throughput limits, with SLAs covering uptime.

For most teams shipping their first real-time Solana product, start with a managed endpoint. You will get to production faster and learn where your actual bottlenecks are. Migrate to self-hosted once you have enough data to size the hardware correctly and enough revenue to justify the operational overhead.


If you want these systems built and running without spending months on infrastructure work, talk to our team — we design, build, and operate production Solana trading infrastructure end to end, including Yellowstone-backed data pipelines for our bot deployments.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#solana#infrastructure#grpc#geyser#yellowstone#trading-bots#streaming#real-time