Reconnects, Gaps and Backfill in Streaming
TL;DR
- - All Solana streams disconnect eventually — design for reconnection from the start
- - Track the last processed slot to detect gaps after reconnection
- - Backfill missed data using RPC getBlock or getTransaction calls
Every Solana streaming connection — gRPC, WebSocket, or custom — will disconnect at some point. Network issues, server maintenance, and client-side errors all cause interruptions. Applications that cannot afford missing data need a strategy for detecting and filling gaps.
The gap problem
When a stream disconnects and reconnects:
Stream active: [slot 100] [slot 101] [slot 102]
Disconnect: ............(gap).............
Reconnect: [slot 108] [slot 109] [slot 110]
Slots 103–107 were processed by the network but your application never received them. Without gap detection, your data has holes.
Gap detection
Track the last slot you processed. On each update, check for continuity:
let lastProcessedSlot = 0;
function onSlotUpdate(slot: number) {
if (lastProcessedSlot > 0 && slot > lastProcessedSlot + 1) {
const missedSlots = [];
for (let s = lastProcessedSlot + 1; s < slot; s++) {
missedSlots.push(s);
}
backfillSlots(missedSlots);
}
lastProcessedSlot = slot;
}
For account-based subscriptions (not slot-based), you may not know exactly which slots were missed. Record the last slot from any update and backfill the range on reconnect.
Backfill strategies
RPC-based backfill
Use RPC methods to fetch missed data:
async function backfillSlots(missedSlots: number[]) {
for (const slot of missedSlots) {
try {
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});
if (block) {
processBlock(slot, block);
}
} catch (err) {
// Slot may have been skipped (no block produced)
console.log(`Slot ${slot} skipped or unavailable`);
}
}
}
Account snapshot on reconnect
For account monitoring, fetch the current account state on reconnect to ensure you have the latest data:
async function onReconnect(trackedAccounts: PublicKey[]) {
const infos = await connection.getMultipleAccountsInfo(trackedAccounts);
for (let i = 0; i < trackedAccounts.length; i++) {
if (infos[i]) {
processAccountUpdate(trackedAccounts[i], infos[i]);
}
}
}
Dual-source architecture
Run the real-time stream alongside periodic RPC polling as a safety net:
gRPC stream (primary) → Process updates in real-time
↕ (compare)
RPC polling (backup) → Fetch state every N seconds, detect discrepancies
Reconnection with exponential backoff
async function connectWithBackoff(
connect: () => Promise<Stream>,
onData: (data: any) => void,
) {
let attempt = 0;
while (true) {
try {
const stream = await connect();
attempt = 0;
for await (const data of stream) {
onData(data);
}
} catch (err) {
attempt++;
const delay = Math.min(1000 * 2 ** attempt, 60_000);
console.log(`Reconnecting in ${delay}ms (attempt ${attempt})...`);
await new Promise(r => setTimeout(r, delay));
}
}
}
Idempotent processing
Backfill may deliver data you already processed (if the gap started mid-slot). Make your processing idempotent:
- Use the transaction signature as a deduplication key.
- Use slot + write version for account updates.
- Store a “last processed” watermark and skip anything at or below it.
FAQ
Can Yellowstone gRPC guarantee no missed updates? No streaming protocol guarantees zero missed updates. gRPC is more reliable than WebSocket, but network interruptions can still cause gaps. Always implement gap detection.
How far back can I backfill? RPC nodes retain block data for a limited time (typically the last 2 epochs, ~4–5 days). For older data, you need an archival node or a data service like Solana’s BigTable.
Should I backfill every time I reconnect? Yes. Even brief disconnections can miss slots. The cost of checking for gaps is low compared to the risk of missing data.
Updated: