# Replay

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

Source: https://docs.constant-k.com/kaldera/replay/

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

## Replay window

Kaldera answers `fromSlot` from two places.

| Source                                | Covers                                                                               |
| :------------------------------------ | :----------------------------------------------------------------------------------- |
| The recent-slot buffer in your region | The last 1,000 slots or so: about 4 to 5 minutes of chain time at current slot times |
| The replay store                      | Up 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`.

**TypeScript**

```ts
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;
```

**Rust**

```rust
let info = client.subscribe_replay_info().await?;
let oldest = info.first_available; // Option<u64>

let from_slot = match (last_slot, oldest) {
    (Some(last), Some(oldest)) => Some(last.max(oldest)),
    (last, _) => last,
};
```

**grpcurl**

```bash
grpcurl \
  -import-path yellowstone-grpc/yellowstone-grpc-proto/proto \
  -proto geyser.proto \
  -H "x-token: YOUR_API_KEY" \
  kaldera-newyork.constant-k.com:443 geyser.Geyser/SubscribeReplayInfo
```

```json
{ "firstAvailable": "447912345" }
```

## 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.

| Update      | Key                             |
| :---------- | :------------------------------ |
| Transaction | Signature                       |
| Account     | Pubkey, slot and `writeVersion` |
| Slot        | Slot and status                 |

## What replay covers

| Updates                             | Replay                                                                    |
| :---------------------------------- | :------------------------------------------------------------------------ |
| Transactions and transaction status | Verified against block history, with nothing missing and nothing repeated |
| Slot status                         | Verified                                                                  |
| Block meta                          | Verified                                                                  |
| Accounts and entries                | Not verified. Replay is not a source of record for these.                 |

Replay is not available on [Turbo Kaldera](https://docs.constant-k.com/kaldera/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.

**TypeScript**

```ts title="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));
}
```

**Rust**

`from_slot` is an `Option<u64>` on `SubscribeRequest`. A template request carries the filters, and `from_slot` is set on each reconnect:

```rust
let resume_request = SubscribeRequest {
    from_slot: last_slot, // Option<u64>: None on the first connect
    ..request_template.clone()
};

let (_sink, mut stream) = client.subscribe_with_request(Some(resume_request)).await?;
```

`last_slot` comes from `update.slot` as each update is handled, alongside a set of recent signatures that skips duplicates, as in the TypeScript example.

From `yellowstone-grpc-client` 14.0.0, `subscribe` and `subscribe_with_request` never reconnect, even with `set_reconnect_config`. The built-in layer is `subscribe_with_reconnect`. It needs `processed` commitment and rejects `from_slot`.

## 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.

```ts
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));
```

| Rule                        | Detail                                                                                                                                                 |
| :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
| Request                     | `processed` commitment, no `fromSlot`, no startup snapshot                                                                                             |
| Fourth constructor argument | `backoff` only. `connect()` rejects `enabled`, `policy` and `slotRetention`.                                                                           |
| Resume point                | After the last finalized slot                                                                                                                          |
| Events                      | `Update` (`generation`, `update`) or `DiscardBanks` (banks to undo, then replacement updates)                                                          |
| Extra filters               | A `slots` and a `blocksMeta` filter, both named `__autoreconnect`. They count toward your [filters-per-stream limit](https://docs.constant-k.com/kaldera/filters/#filter-limits). |
| End of stream               | `OUT_OF_RANGE` or exhausted retries end the stream with an error                                                                                       |
| Memory                      | Recovery 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:

```text
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](https://docs.constant-k.com/get-started/plans-and-limits/#kaldera-grpc-limits).
