Replay

The Kaldera replay window, the fromSlot parameter, and resuming a stream after a disconnect.

Kaldera keeps recent history, so a reconnecting client can replay what it missed.

Replay window

Kaldera answers fromSlot from two places.

SourceCovers
The recent-slot buffer in your regionThe last 1,000 slots or so: about 4 to 5 minutes of chain time at current slot times
The replay storeUp to 24 hours, on Blade Runner and on Nexus

Not every region keeps its own replay store. A region without one is served from a region that has one, so the replayed part of the stream carries extra round-trip latency until it catches up. If no store is reachable, the region falls back to its recent-slot buffer.

24 hours is a span, not a promise

The replay store spans up to 24 hours, but a break in the feed or a restart leaves a hole inside that span. A hard-coded window is unsafe: the server reports the oldest slot it can serve, and nothing earlier can be requested.

The available span

SubscribeReplayInfo is a single request and response, not a stream. It uses the same endpoint and API key as Subscribe, and returns firstAvailable: the oldest slot that region can replay from. On reconnect, the slot to use is the larger of your own last slot and firstAvailable.

const client = new Client(ENDPOINT, process.env.CK_API_KEY, undefined);
await client.connect();

const info = await client.subscribeReplayInfo(); // { firstAvailable?: string }
const oldest = info.firstAvailable === undefined ? undefined : BigInt(info.firstAvailable);

// Never ask for a slot the server cannot serve
const from = lastSlot !== undefined && oldest !== undefined && lastSlot < oldest ? oldest : lastSlot;

Resuming a stream

fromSlot on a SubscribeRequest starts the stream at that slot. Kaldera first sends the matching updates from there, then continues with live data on the same stream. If the server loses its own source in the middle of a replay, it resumes at the last slot it sent you, not at your original fromSlot, so you do not receive the replay twice.

  1. Record the highest slot you have handled.
  2. Resume from that slot, not the next one. A slot's updates can be split across the moment you disconnected.
  3. Deduplicate by a stable key. Replay can resend updates you already handled.
UpdateKey
TransactionSignature
AccountPubkey, slot and writeVersion
SlotSlot and status

What replay covers

UpdatesReplay
Transactions and transaction statusVerified against block history, with nothing missing and nothing repeated
Slot statusVerified
Block metaVerified
Accounts and entriesNot verified. Replay is not a source of record for these.

Replay is not available on Turbo Kaldera: a request that carries fromSlot is rejected with INVALID_ARGUMENT. Standard Kaldera covers the gap instead.

Throughput of a deep replay

A replay runs as fast as your client reads, far faster than live traffic. A consumer that cannot keep up overflows its buffer and loses the stream.

  • A general-purpose command-line client drains roughly 440 messages a second. That is not enough for a transaction filter over a few thousand slots.
  • The socket belongs on its own task or thread, with decoding and business logic elsewhere.
  • The narrowest filter that closes the gap replays faster than a full production filter.
  • Hours of history rather than minutes are paged: a few hundred slots, a checkpoint, then the next range.

A stream that survives disconnects

This subscriber reconnects with exponential backoff, resumes from the last slot it saw, and skips duplicates.

resilient-stream.ts
import Client, {
  CommitmentLevel,
  type SubscribeRequest,
  type SubscribeUpdate,
} from "@triton-one/yellowstone-grpc";
import bs58 from "bs58";

const ENDPOINT = "https://kaldera-newyork.constant-k.com";

const request: SubscribeRequest = {
  commitment: CommitmentLevel.CONFIRMED,
  transactions: {
    pumpfun: {
      vote: false,
      accountInclude: ["6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P"],
      accountExclude: [],
      accountRequired: [],
    },
  },
  accounts: {},
  slots: {},
  transactionsStatus: {},
  blocks: {},
  blocksMeta: {},
  entry: {},
  blockFooter: {},
  accountsDataSlice: [],
};

let lastSlot: bigint | undefined; // highest slot seen so far
const seen = new Map<string, bigint>(); // signature -> slot, to drop replayed duplicates

function handleTransaction(signature: string, slot: bigint) {
  console.log(slot, signature);
}

async function runStream(): Promise<void> {
  const client = new Client(ENDPOINT, process.env.CK_API_KEY, undefined);
  await client.connect();

  // Resume from the last slot you saw. fromSlot is a decimal string.
  const stream = await client.subscribe(
    lastSlot === undefined ? request : { ...request, fromSlot: lastSlot.toString() },
  );

  await new Promise<void>((resolve) => {
    stream.on("data", (update: SubscribeUpdate) => {
      const tx = update.transaction;
      if (!tx?.transaction) return;

      const slot = BigInt(tx.slot);
      const signature = bs58.encode(tx.transaction.signature);
      if (seen.has(signature)) return; // already handled before the reconnect

      seen.set(signature, slot);
      handleTransaction(signature, slot);
      if (lastSlot === undefined || slot > lastSlot) lastSlot = slot;

      // Keep memory bounded: forget signatures from old slots
      if (seen.size > 200_000) {
        for (const [sig, s] of seen) if (s + 1_000n < slot) seen.delete(sig);
      }
    });
    stream.on("error", (err) => console.error("stream error:", err.message));
    stream.on("close", () => resolve());
  });
}

// Reconnect forever, with capped exponential backoff
for (let attempt = 0; ; attempt++) {
  const started = Date.now();
  await runStream().catch((err) => console.error("connect failed:", err.message));
  if (Date.now() - started > 60_000) attempt = 0; // it ran for a while: reset the backoff
  const delay = Math.min(500 * 2 ** attempt, 30_000);
  await new Promise((resolve) => setTimeout(resolve, delay));
}

Built-in reconnect in the TypeScript client

@triton-one/yellowstone-grpc 8.0.0 and later have a built-in reconnect layer, subscribeWithReconnect(). Plain subscribe() never reconnects.

import Client, { CommitmentLevel, type ReconnectEvent } from "@triton-one/yellowstone-grpc";

const client = new Client(
  "https://kaldera-newyork.constant-k.com",
  process.env.CK_API_KEY,
  undefined,
  { backoff: { initialIntervalMs: 100, multiplier: 2, maxRetries: 10 } }, // backoff only
);
await client.connect();

// processed commitment, no fromSlot: the layer picks the replay point
const stream = await client.subscribeWithReconnect({ ...request, commitment: CommitmentLevel.PROCESSED });

stream.on("data", (event: ReconnectEvent) => {
  if (event.type === "Update") {
    handleUpdate(event.update);
  } else {
    // DiscardBanks: undo what you stored for these banks; replacement updates follow
    for (const bank of event.banks) dropBank(bank.slot, bank.bankId);
  }
});
stream.on("error", (err) => console.error("recovery failed:", err.message));
RuleDetail
Requestprocessed commitment, no fromSlot, no startup snapshot
Fourth constructor argumentbackoff only. connect() rejects enabled, policy and slotRetention.
Resume pointAfter the last finalized slot
EventsUpdate (generation, update) or DiscardBanks (banks to undo, then replacement updates)
Extra filtersA slots and a blocksMeta filter, both named __autoreconnect. They count toward your filters-per-stream limit.
End of streamOUT_OF_RANGE or exhausted retries end the stream with an error
MemoryRecovery buffers updates with no size cap while it waits for finalized slots

The layer deduplicates its own backfill, not your business logic. Your own idempotency still matters where a repeated update would cost money.

If the slot is too old

A fromSlot outside the window cannot be served. The stream fails with gRPC status OUT_OF_RANGE (11) and a message that names the oldest slot you can ask for:

broadcast from 447000000 is not available, last available: 447700000

That second number is the oldest available slot, not the newest. It is the same value SubscribeReplayInfo returns, and a client that retries the same too-old fromSlot gets the same error every time.

  1. Resubscribe from the oldest available slot, or without fromSlot to go straight to live data.
  2. Fill the gap from Solana RPC if you need it, for example with getSignaturesForAddress and getTransaction for the accounts you track.

Other ways to avoid gaps

  • Redundant streams. The same filters in more than one region, or twice in one region, with deduplication.
  • Consumer speed. On Blade Runner, a client that falls far behind loses the oldest queued updates. See Plans & Limits.

On this page