Never miss a beat: Yellowstone gRPC's historical replay lets you recover from disconnections and backfill missing data from the last ~48 hours of Solana blockchain activity.
Historical replay is a Yellowstone gRPC feature that lets you resume streaming from any slot within the last 432,000 slots (approximately 48 hours of blockchain activity). This is useful for handling brief disconnections and keeping data continuous in real-time apps without opening a separate backfill pipeline.
Limited time window: Historical replay is limited to the last 432,000 slots (~48 hours) of blockchain activity. You cannot replay data from arbitrary points further in the past.
Recover data lost during brief disconnections (up to ~48 hours).
Start applications with recent context from the last few minutes.
Review recent transactions and account changes.
Use real recent data for testing and development.
Set the from_slot field on SubscribeRequest to your replay starting slot. It must fall within the last ~432,000 slots.
Yellowstone gRPC delivers all matching events from your specified slot forward.
Historical data streams until you reach the current slot.
The connection transitions seamlessly into real-time streaming; no reconnect required.
from_slot must fall within the last ~432,000 slots (~48 hours), so the example below derives it dynamically by calling getSlot on the Solana RPC and rewinding ~1000 slots (~7 minutes). Do not hard-code an absolute slot number, since any fixed value ages out of the replay window as the chain advances.
use anyhow::Result;
use futures::{sink::SinkExt, stream::StreamExt};
use serde_json::json;
use std::collections::HashMap;
use yellowstone_grpc_client::{ClientTlsConfig, GeyserGrpcClient};
use yellowstone_grpc_proto::geyser::{
CommitmentLevel, SubscribeRequest, SubscribeRequestFilterTransactions,
};
async fn get_current_slot(api_key: &str) -> Result<u64> {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "getSlot",
"params": [{ "commitment": "confirmed" }],
});
let res = reqwest::Client::new()
.post(format!("https://solana-mainnet.g.alchemy.com/v2/{}", api_key))
.json(&body)
.send()
.await?
.json::<serde_json::Value>()
.await?;
Ok(res["result"].as_u64().unwrap_or_default())
}
#[tokio::main]
async fn main() -> Result<()> {
let endpoint = "https://solana-mainnet.g.alchemy.com";
let api_key = "ALCHEMY_API_KEY"; // Replace with your Alchemy API key
let mut client = GeyserGrpcClient::build_from_shared(endpoint)?
.tls_config(ClientTlsConfig::new().with_native_roots())?
.x_token(Some(api_key))? // API key passed as X-Token header
.connect()
.await?;
let (mut tx, mut stream) = client.subscribe().await?;
// Derive from_slot from the current slot so it always sits inside the
// ~432,000 slot replay window. Here we rewind ~1000 slots (~7 minutes);
// tune the offset to how much history you want to replay.
let current_slot = get_current_slot(api_key).await?;
let from_slot = current_slot.saturating_sub(1_000);
tx.send(SubscribeRequest {
transactions: HashMap::from([(
"all_transactions".to_string(),
SubscribeRequestFilterTransactions {
vote: Some(false), // Exclude vote transactions
failed: Some(false), // Exclude failed transactions
..Default::default()
},
)]),
commitment: Some(CommitmentLevel::Confirmed as i32),
from_slot: Some(from_slot), // Start streaming from this historical slot
..Default::default()
})
.await?;
while let Some(Ok(update)) = stream.next().await {
println!("Received update: {:?}", update);
}
Ok(())
}The slot number to start replaying from, as a u64. Must be within the replay window (last ~432,000 slots from the current slot).
Type: optional uint64
Example: current_slot - 1000 (replays roughly the last 7 minutes)
Behavior:
- If specified, streaming begins from this slot number.
- All matching data from
from_slotonwards is sent, then the connection transitions to real-time. - If not specified, streaming begins from the current slot.
- If the requested slot is older than the ~432,000 slot window, the server rejects the request.
See the Subscribe request reference for the full SubscribeRequest message definition.
Reconnection after a brief disconnection
When your application reconnects after a disconnection (under ~48 hours), historical replay makes sure no data is missed. The example below tracks the last processed slot in a RwLock; persist it however suits your app (Redis, Postgres, a file, etc.) so the next reconnect can pick up where you left off.
use anyhow::Result;
use serde_json::json;
async fn get_current_slot(api_key: &str) -> Result<u64> {
let body = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "getSlot",
"params": [{ "commitment": "confirmed" }],
});
let res = reqwest::Client::new()
.post(format!("https://solana-mainnet.g.alchemy.com/v2/{}", api_key))
.json(&body)
.send()
.await?
.json::<serde_json::Value>()
.await?;
Ok(res["result"].as_u64().unwrap_or_default())
}
// Load the last slot you processed from wherever you store it.
let mut last_processed_slot: u64 = load_from_persistent_store().unwrap_or(0);
// Check whether it's still within the replay window.
let current_slot = get_current_slot("ALCHEMY_API_KEY").await?;
let max_replay_slot = current_slot.saturating_sub(432_000);
if last_processed_slot < max_replay_slot {
eprintln!("Disconnection too long, some data may be lost");
last_processed_slot = max_replay_slot;
}
tx.send(SubscribeRequest {
// ... your subscription config
from_slot: Some(last_processed_slot),
..Default::default()
})
.await?;Bootstrap with recent context
Start your application with recent context from the last few minutes:
let current_slot = get_current_slot("ALCHEMY_API_KEY").await?;
let start_slot = current_slot.saturating_sub(1_500); // ~10 minutes ago
tx.send(SubscribeRequest {
// ... your subscription config
from_slot: Some(start_slot),
..Default::default()
})
.await?;Testing with recent data
Use recent historical data for testing (limited to the last ~48 hours):
use yellowstone_grpc_proto::geyser::subscribe_update::UpdateOneof;
let current_slot = get_current_slot("ALCHEMY_API_KEY").await?;
let test_start_slot = current_slot.saturating_sub(750); // ~5 minutes ago
let test_end_slot = current_slot.saturating_sub(150); // ~1 minute ago
tx.send(SubscribeRequest {
// ... your subscription config
from_slot: Some(test_start_slot),
..Default::default()
})
.await?;
while let Some(Ok(update)) = stream.next().await {
// Extract the slot from whichever update variant is present.
let slot = match &update.update_oneof {
Some(UpdateOneof::Transaction(t)) => t.slot,
Some(UpdateOneof::Account(a)) => a.slot,
Some(UpdateOneof::Slot(s)) => s.slot,
_ => continue,
};
// Stop processing once we reach the test end slot.
if slot >= test_end_slot {
break;
}
// your test handler here
}When you use from_slot for gap recovery, you may receive updates you've already processed. Use a time-bounded cache or database unique constraints to deduplicate efficiently. See Handle duplicate updates in the best practices guide for more.
For a complete production-ready client that combines automatic reconnection, gap recovery with from_slot, and separate ingress and processing tasks, see the durable client example.