Yellowstone gRPC Auto-Reconnect: Why Your Client Loses Data and How to Stop It
Every Yellowstone gRPC client reconnects. Almost none of them recover. The difference matters because a reconnect that resubscribes at the live head does not "resume" your stream — it abandons every slot produced while you were away, and it does so without an error, a log line, or any signal that data is missing.
We built a fault-injecting mock stream to measure exactly what that costs. The harness kills connections in four different ways and runs three client strategies against each. The results are published in full on the reconnect benchmark; this article is about what to do with them.
The three strategies, and what each one actually recovers
Worst case across every failure mode, not the average — an average tells you nothing about whether you missed the one launch you cared about:
| Client strategy | Worst coverage | Worst gap | Duplicates |
|---|---|---|---|
Naive reconnect Reconnect and subscribe from the live head. The default in most tutorials, including ones we have published. | 85.0% | 17 slots | 1420 |
Resume from last slot Track the last processed slot and resubscribe from it. What a provider replay buffer or Triton-style auto-reconnect buys you. | 100.0% | 0 slots | 1500 |
Dual overlapping streams Keep two subscriptions open permanently and deduplicate by signature. Costs double and cannot be beaten on coverage. | 100.0% | 0 slots | 5632 |
Worst case across 4 fault-injection scenarios, 3 repeats each, verified 2026-10-01. These measure client-side reconnect logic against a mock stream, not named providers — we hold no accounts on competitors and do not measure their reconnect behaviour.
Read that table as a cost curve rather than a ranking. A naive reconnect is free and loses data. Resuming from your last processed slot recovers everything but hands you duplicates at the boundary. Two permanent overlapping subscriptions cannot be beaten on coverage and cost exactly double.
Failure mode 1: the clean close
The socket closes with a FIN. Your client learns immediately, and any reconnect loop handles it. This is the case tutorials are written against, including ones we have published, and it is the least interesting one.
Failure mode 2: the abrupt reset
The connection is destroyed without a FIN — a load balancer recycling, a node dropping out, a network path changing. Your client finds out on a failed read, so detection itself costs time. It is still self-announcing, which is the important property.
Failure mode 3: the silent stall — the one that hurts
The TCP connection stays open, HTTP/2 stays healthy, and the stream simply stops delivering messages. Nothing throws. Your for await loop sits there waiting for a message that will never arrive, and your bot reports itself healthy the entire time.
No transport-level mechanism saves you here. The only defence is a read-idle timeout in your own code: if no message arrives within a window you choose, treat the stream as dead and tear it down yourself.
// Node.js — the check most clients are missing.
// Solana produces slots roughly every 400ms, so silence measured in
// seconds is not a quiet market. It is a dead stream.
const READ_IDLE_MS = 5_000;
let lastMessageAt = Date.now();
const watchdog = setInterval(() => {
if (Date.now() - lastMessageAt > READ_IDLE_MS) {
clearInterval(watchdog);
stream.destroy(new Error("read-idle timeout")); // force the reconnect path
}
}, 1_000);
stream.on("data", (update) => {
lastMessageAt = Date.now();
handle(update);
});
Pick the window against your own tolerance, not ours. Five seconds is roughly a dozen slots — aggressive enough that a stall does not go unnoticed, loose enough that a brief hiccup does not trigger a reconnect storm.
Failure mode 4: repeated flapping
Four faults in quick succession, to check whether reconnect backoff becomes the outage. This is where a fixed one-second retry and an uncapped exponential backoff both fail, from opposite directions: the first hammers a recovering endpoint, the second is still sleeping long after the endpoint came back.
// Capped exponential backoff with jitter.
// The cap matters more than the curve: without it, the fourth failure in a
// flap sequence puts you to sleep for a minute on a stream that is already up.
const BASE_MS = 250;
const MAX_MS = 5_000;
function delayFor(attempt: number): number {
const exponential = Math.min(BASE_MS * 2 ** attempt, MAX_MS);
return exponential / 2 + Math.random() * (exponential / 2); // jitter
}
Resuming properly: track the slot, then deduplicate
Resuming means resubscribing from the last slot you finished processing — not the last slot you received, which may have been partially handled when the connection died. Providers that support a from_slot parameter, and providers with a replay buffer, are both giving you the same thing: the right to ask for the past.
let lastProcessedSlot: bigint | null = null;
function subscribeRequest() {
return {
transactions: { pumpfun: { accountInclude: [PUMP_FUN_PROGRAM] } },
commitment: CommitmentLevel.PROCESSED,
// Only set on reconnect. On a first connect this is undefined and you
// start at the head, which is what you want.
fromSlot: lastProcessedSlot ?? undefined,
};
}
// Resuming re-delivers the boundary slot, so dedupe before you act.
// A bot that buys twice on one signature is worse than one that missed it.
const seen = new Set<string>();
function handle(update) {
const sig = signatureOf(update);
if (seen.has(sig)) return;
seen.add(sig);
process(update);
lastProcessedSlot = BigInt(update.slot);
}
Bound that Set in production — a ring buffer of the last few thousand signatures, or a slot-keyed map you prune behind the boundary. An unbounded set is a memory leak with a long fuse.
What to check in your own client today
- Is there a read-idle timeout? If not, a silent stall is an outage you will not detect.
- Is backoff capped and jittered? An uncapped curve turns a flap into a long outage.
- Do you track the last processed slot, and resubscribe from it?
- Do you deduplicate by signature before acting? Resuming guarantees duplicates.
- Do you log gap size on every reconnect? A silent recovery hides how much you lost.
Where providers differ
Only one item on that list is not entirely yours: whether the endpoint will serve you the past at all. Helius LaserStream keeps roughly 24 hours of replay and fails over between nodes automatically. Triton's Fumarole is built around persistent, resumable streams. Subglow has no replay buffer, so a gap on our stream is a gap you backfill through JSON-RPC or lose — which is the honest reason our own benchmark page recommends someone else for workloads where a missed slot is unacceptable.
Related
- Reconnect benchmark — full methodology, all four scenarios, raw CSV
- Backpressure in Yellowstone gRPC — the failure mode that looks like latency
- Yellowstone gRPC tutorial — connect and subscribe from scratch
Ready to try it?
Get your API key and start receiving filtered data in under 5 minutes. Free tier available.
Get started →