Alpenglow: What Solana RPC and gRPC Users Need to Change

Alpenglow is live on Solana mainnet. What RPC and Yellowstone gRPC users must change: confirmed vs finalized, bank_id, and votes leaving blocks.

  • October 28, 2026
  • Infrastructure
  • AllenHark Team
A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.

Alpenglow is Solana's new consensus protocol, and it is live on mainnet. If your code only sends transactions and reads accounts, you have almost nothing to do. If you stream blocks, run an indexer, chart TPS or wait for confirmed, a few things changed under you. This post covers what changed, who has to act, and the code for each case. Every protocol fact links to its primary source.

What changed

Alpenglow's first phase, Votor, replaced TowerBFT. According to the Solana Foundation's Alpenglow page, the feature gate activated at the start of epoch 1064, on October 21, 2026, at about 13:36 UTC. The cluster ran TowerBFT for 5,000 more slots, slot 459,652,999 became the Alpenglow genesis block, and Alpenglow consensus has run from slot 459,653,000. Validators no longer vote with transactions. They send votes directly to each other and aggregate them into certificates, so a block is final after one or two rounds of voting (SIMD-0326). The Solana Foundation puts finality at roughly 150 ms, down from about 12.8 seconds, and calls the 150 ms an expectation rather than a constant. Execution did not change: the SVM, transaction formats and fees are the same. Three things changed for anyone reading the chain: confirmed and finalized now describe the same state, one slot can carry more than one candidate block (Geyser and Yellowstone gRPC label each one with a bank_id), and vote transactions are no longer in blocks.

[!NOTE] You can check the switch yourself. Agave 4.3 added the getAgGenesisCert RPC method: it returns null while a cluster still runs TowerBFT, and the genesis certificate once it has migrated. On mainnet it returns the certificate for slot 459,652,999.

Terminal
curl -s https://api.mainnet-beta.solana.com -X POST \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"getAgGenesisCert"}'

Who needs to change what

You runWhat changedWhat to do
A send-only bot (sendTransaction, a relay)Nothing in how you build, sign or send. Confirmation arrives sooner.Nothing is required. Accept finalized wherever you accept confirmed, and shorten timeouts tuned to 12.8-second finality.
An account reader (getAccountInfo, accountSubscribe, gRPC account filters)Reads work as before. processed still means "executed in a bank", not final.No code change. Read at finalized where you used confirmed.
A block streamer (Yellowstone blocks, block meta, transactions or entries at processed)A slot can have more than one candidate bank, and each event carries a bank_id.Buffer by slot and bank_id, keep the bank that reaches Confirmed, drop the rest. Regenerate stubs from the current proto.
An indexer or a TPS dashboardVote transactions are gone from blocks, so counts dropped sharply.Re-baseline counts and charts. Take validator participation from block footer certificates.
A wallet or exchange reading confirmedconfirmed and finalized are the same state, and confirmed is to be removed in a later release.Move to finalized. Re-check confirmation waits and UX delays built around 12.8 seconds.
A shred reader timing the leader from ticksProof of History no longer paces blocks, so tick entries left the shred stream.Keep your own slot clock instead of counting ticks.

Two smaller changes from the same page: the Clock sysvar's unix_timestamp is now set by the leader within a bounded range instead of being the stake-weighted median of vote timestamps, and slot times were cut separately, from 400 ms to 200 ms in four stages that finished in October 2026.

Commitment levels, before and after

LevelUnder TowerBFTUnder Alpenglow
processedExecuted in a bank on this node; can still be droppedUnchanged
confirmedOptimistic supermajority, about one slot outThe same state as finalized
finalizedAfter 32 slots of vote lockout, about 12.8 sA finalization certificate exists, target about 150 ms

Anza did not redefine confirmed as first-round notarization. It kept the meaning and let the gap close. In Agave 4.3, when Votor roots a block it also sends the optimistic-confirmation notification for that slot "to keep compatibility with the RPC API", and the code comment says they will look to deprecate it later. The Solana Foundation says confirmed will be removed in a later release. No release or date has been named.

We checked this on October 28, 2026 with one batched getSlot request at all three levels against api.mainnet-beta.solana.com: confirmed and finalized returned the same slot every time, with processed up to two slots ahead.

What that means in practice:

  • Point new code at finalized. It no longer costs you seconds.
  • A finalized blockhash is no longer old. Under TowerBFT it trailed by about 32 slots, which ate into a transaction's lifetime. That reason to fetch blockhashes at confirmed is gone.
  • Don't test for "confirmed" by equality. A signature can show finalized the first time you look. Fresh signatures we looked up on October 28 already reported finalized.
  • Watch library defaults. @solana/kit fills in confirmed when a call names no commitment (source). Rust's RpcClient::new defaults to finalized.

@solana/web3.js

TypeScript
import { Connection, PublicKey } from "@solana/web3.js";

// The connection-wide default; any call can still pass its own
const connection = new Connection("http://88.216.36.108:7999", { commitment: "finalized" });

const account = await connection.getAccountInfo(new PublicKey("YOUR_PUBKEY"), "finalized");
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("finalized");

// ...build, sign and send with that blockhash, then:
await connection.confirmTransaction({ signature, blockhash, lastValidBlockHeight }, "finalized");

// Polling statuses yourself? Accept finalized wherever you accepted confirmed
const landed = (s) => s?.confirmationStatus === "confirmed" || s?.confirmationStatus === "finalized";

@solana/kit

TypeScript
import {
  address,
  createSolanaRpc,
  createSolanaRpcSubscriptions,
  sendAndConfirmTransactionFactory,
} from "@solana/kit";

const rpc = createSolanaRpc("http://88.216.36.108:7999");
const rpcSubscriptions = createSolanaRpcSubscriptions("ws://88.216.36.108:7999");

// Kit applies "confirmed" when a call names no commitment, so name it
const { value: account } = await rpc
  .getAccountInfo(address("YOUR_PUBKEY"), { commitment: "finalized", encoding: "base64" })
  .send();
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: "finalized" }).send();

const sendAndConfirm = sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions });
await sendAndConfirm(signedTransaction, { commitment: "finalized" });

Rust (solana-client)

Rust
// Cargo.toml: solana-client = "4.3", solana-commitment-config = "3.1"
use solana_client::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // RpcClient::new already defaults to finalized; this makes it explicit
    let rpc = RpcClient::new_with_commitment(
        "http://88.216.36.108:7999".to_string(),
        CommitmentConfig::finalized(),
    );

    let blockhash = rpc.get_latest_blockhash()?; // uses the client's commitment
    let tip = rpc.get_slot_with_commitment(CommitmentConfig::processed())?; // per-call override
    println!("blockhash {blockhash}, processed slot {tip}");
    Ok(())
}

One open issue to know about if you read accounts at processed over WebSocket: Agave #16357 reports that accountSubscribe does not correct a processed notification after a same-slot duplicate block is swapped. A fix is in review. If a processed value moves money, re-read it at finalized first.

bank_id: one slot, more than one candidate block

A bank is a validator's working state for one candidate block. A slot gets a second bank in two ways. Equivocation, where a leader signs two different blocks for one slot, is the only way under Agave 4.3, and it is rare. Fast leader handover, which ships with Agave 4.4 (v4.4.0-rc.1 was tagged on October 26), lets a leader restart its block on a different parent; Anza expects that in under 1% of slots. Agave 4.3 added bank_id a release early so streaming code can get this right while it is still rare.

In the Yellowstone proto the field is bank_id, a uint64:

MessageFieldNotes
SubscribeUpdateAccountoptional uint64 bank_id = 4Unset for startup snapshot accounts
SubscribeUpdateSlotoptional uint64 bank_id = 5Unset for SLOT_FIRST_SHRED_RECEIVED, SLOT_COMPLETED and SLOT_DEAD
SubscribeUpdateTransactionuint64 bank_id = 3
SubscribeUpdateTransactionStatusuint64 bank_id = 6
SubscribeUpdateBlockuint64 bank_id = 14Added in yellowstone-grpc-proto 14.0.0
SubscribeUpdateBlockMetauint64 bank_id = 10
SubscribeUpdateEntryuint64 bank_id = 7

The rules, from the Solana Foundation page and the Yellowstone changelog:

  • Key everything by slot and bank_id, never by slot alone. Keyed by slot, two competing candidates fuse into one block and nothing flags it.
  • Keep the bank that reaches Confirmed and drop the others. Votor picks the winner; you never arbitrate. A processed subscriber can get two blocks for one slot, told apart by bank_id. Confirmed and Finalized are delivered once per slot, for the bank that reached them.
  • SLOT_DEAD carries no bank_id. Drop every candidate for that slot. A repaired slot can come back with a new bank_id.
  • bank_id is a counter local to one validator process. It means nothing on another connection, and ids restart when the validator restarts. Clear your bank state on every reconnect. To merge feeds from several endpoints, keep one buffer per connection and reconcile on the blockhash from block meta.

The simplest fix: if you can wait about 150 ms, subscribe at confirmed or finalized. You get one bank per slot, in order: its data, then block meta, then the slot status. Under TowerBFT that cost you seconds; now it doesn't. If you need processed, buffer like this:

JavaScript
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

// geyser.proto and solana-storage.proto from rpcpool/yellowstone-grpc, side by side
const def = protoLoader.loadSync("geyser.proto", {
  keepCase: true, longs: String, enums: String, defaults: true, oneofs: true,
});
const { geyser } = grpc.loadPackageDefinition(def);
const client = new geyser.Geyser("88.216.36.108:10001", grpc.credentials.createInsecure());

const PUMP = "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P";
const delivered = new Map(); // slot -> blockhash already handed on; survives reconnects
let lastConfirmed = 0;

function onBlock(slot, blockhash, txs) { /* your code: one winning bank per slot */ }

function connect(fromSlot) {
  // bank_id only means something on this connection, so the buffers live here
  const banks = new Map(); // "slot:bank_id" -> { slot, bankId, txs, meta, won }
  const get = (slot, bankId) => {
    const k = `${slot}:${bankId}`;
    if (!banks.has(k)) banks.set(k, { slot, bankId, txs: [], meta: null, won: false });
    return banks.get(k);
  };
  const drop = (b) => banks.delete(`${b.slot}:${b.bankId}`);
  const finish = (b) => {
    if (!b.won || !b.meta) return; // wait for the win and the block meta
    if (delivered.get(b.slot) !== b.meta.blockhash) { // a replayed copy is skipped
      delivered.set(b.slot, b.meta.blockhash);
      onBlock(b.slot, b.meta.blockhash, b.txs);
    }
    drop(b);
  };
  const prune = () => { // slots far behind the last confirmed one were skipped
    for (const b of [...banks.values()]) if (Number(b.slot) < lastConfirmed - 150) drop(b);
    for (const s of [...delivered.keys()]) if (Number(s) < lastConfirmed - 150) delivered.delete(s);
  };

  // From an allowlisted IP the metadata stays empty; with an API key, add it as x-token
  const meta = new grpc.Metadata();
  if (process.env.ALLENHARK_KEY) meta.set("x-token", process.env.ALLENHARK_KEY);
  const stream = client.subscribe(meta);

  stream.on("data", (u) => {
    if (u.update_oneof === "transaction") {
      get(u.transaction.slot, u.transaction.bank_id).txs.push(u.transaction.transaction);
    } else if (u.update_oneof === "block_meta") {
      const b = get(u.block_meta.slot, u.block_meta.bank_id);
      b.meta = u.block_meta;
      finish(b);
    } else if (u.update_oneof === "slot") {
      const { slot, status, bank_id } = u.slot;
      if (status === "SLOT_CONFIRMED" && bank_id != null) {
        lastConfirmed = Math.max(lastConfirmed, Number(slot));
        for (const b of [...banks.values()]) {
          if (b.slot === slot && b.bankId !== bank_id) drop(b); // the replaced candidate
        }
        const winner = get(slot, bank_id);
        winner.won = true;
        finish(winner);
        prune();
      } else if (status === "SLOT_DEAD") {
        for (const b of [...banks.values()]) if (b.slot === slot) drop(b);
      }
    }
  });

  let closed = false;
  const reconnect = () => {
    if (closed) return;
    closed = true;
    // Resume just before the last confirmed slot; whatever was already delivered is skipped by blockhash
    setTimeout(() => connect(lastConfirmed ? String(lastConfirmed - 3) : undefined), 1000);
  };
  stream.on("error", reconnect);
  stream.on("end", reconnect);

  stream.write({
    accounts: {},
    slots: { slots: {} },
    transactions: {
      pump: { vote: false, failed: false, account_include: [PUMP], account_exclude: [], account_required: [] },
    },
    transactions_status: {},
    blocks: {},
    blocks_meta: { meta: {} },
    entry: {},
    accounts_data_slice: [],
    commitment: "PROCESSED",
    ...(fromSlot ? { from_slot: fromSlot } : {}),
  });
}

connect();

A few notes on the sketch. It waits for both the Confirmed status and the block meta, because at processed Yellowstone makes no ordering promise between the two. With an unfiltered stream you can also seal a bank once your counts match executed_transaction_count and entries_count in its block meta; with a filtered one like this, the Confirmed status is your signal. The leader can also announce an abandoned bank early: setting include_update_parent on an entry filter adds SubscribeUpdateEntryUpdateParent, whose cleared_bank_id names it. The Confirmed rule already drops that bank, so it is optional. In Rust, yellowstone-grpc-client 14.0.0 and later has subscribe_with_reconnect, a bank-aware reconnecting stream that reports partly delivered banks to discard after a reconnect.

Vote transactions left the block

Under TowerBFT every validator voted with a transaction in every slot, and those votes filled much of each block. In block 459,652,999, the last TowerBFT block, 649 of 1,051 transactions called the Vote program. In the next block, 459,653,008, none of 1,701 did. We read both with getBlock; anyone can repeat it.

What that does to your numbers:

  • Total TPS fell overnight with no drop in real activity. Any chart, alert or benchmark whose baseline included votes needs a new one.
  • getRecentPerformanceSamples shows it directly. On October 28, numTransactions equaled numNonVoteTransactions in every sample we fetched. For a series that spans the switch, plot numNonVoteTransactions: it means the same thing before and after.
  • Per-slot metrics moved for a second reason. Slots are now about 200 ms, so per-slot and per-second figures changed independently of the votes.
  • Vote filters are now no-ops. vote: false in a Yellowstone filter still works; there is nothing left for it to remove.
  • Validator participation needs a new source. It now lives in certificates in the Alpenglow block footer, not in Vote program instructions. Current Yellowstone protos add a block_footer filter with an opt-in include_certificates.

Checklist

  • Move confirmed reads and confirmations to finalized, and name the commitment explicitly in Kit.
  • Accept finalized anywhere your code checks for confirmed.
  • Re-check timeouts, retries and deposit waits tuned to 12.8-second finality.
  • Regenerate Yellowstone stubs from the current geyser.proto (yellowstone-grpc-proto 14.x in Rust).
  • Key stream buffers by slot and bank_id; keep the Confirmed bank; drop the rest, and everything on SLOT_DEAD.
  • Clear bank state on reconnect, and never compare bank_id across connections.
  • Re-baseline transaction counts and TPS; chart numNonVoteTransactions across the switch.
  • Replace tick counting in shreds with a local slot clock.
  • Gate Alpenglow-specific behavior on getAgGenesisCert, not on a date.

AllenHark gRPC and RPC on Alpenglow

AllenHark's Yellowstone gRPC streams carry bank_id and support Alpenglow. This is live now, so the buffering above works against our endpoints as written.

  • Endpoints: Frankfurt 88.216.36.108:10001 and Amsterdam 84.32.104.38:10001, plaintext HTTP/2. Connect with http:// (or createInsecure()), never https://, and keep the endpoint inside your own infrastructure.
  • Authentication: connect from an IP address on your allowlist, or, on new subscriptions, send your API key in the x-token header. See the gRPC docs.
  • Replay after a reconnect: set from_slot to resume where you left off, as the sketch does. AllenHark keeps about 48 hours, so even a long outage can be replayed. SubscribeReplayInfo returns first_available, the oldest slot you can ask for.
  • Rate limits: standard AllenHark RPC is 100 requests per second. Dedicated RPC and Dedicated gRPC have no rate limits. Plans are on the pricing page.

The Solana gRPC reference covers every subscription filter, and the RPC reference covers the JSON-RPC methods. If you read raw shreds, remember that tick entries are gone from them.

FAQ

Is Alpenglow live on mainnet?

Yes. The feature gate activated at the start of epoch 1064 on October 21, 2026, and Alpenglow consensus has run since slot 459,653,000. getAgGenesisCert confirms it on any cluster.

Should I switch from confirmed to finalized?

Yes, for new code and as you touch old code. They now describe the same state, finalized arrives in roughly 150 ms, and confirmed is to be removed in a later release.

Why did Solana's TPS drop?

Vote transactions left the block. Real activity did not fall. Non-vote counts are comparable across the switch.

What is bank_id in my gRPC stream?

A number that tells apart competing candidate blocks for the same slot, assigned by the validator you stream from. Key by slot and bank_id, keep the bank that reaches Confirmed, and never compare bank_id across connections.

Do I need to change anything if I only send transactions?

No. Transaction formats, fees and execution are unchanged. Update confirmation checks to accept finalized.

Do JSON-RPC and WebSocket responses include bank_id?

No. bank_id exists only on Geyser and gRPC.

Related reading

Sources

Checked October 28, 2026.