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.
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
getAgGenesisCertRPC method: it returnsnullwhile a cluster still runs TowerBFT, and the genesis certificate once it has migrated. On mainnet it returns the certificate for slot 459,652,999.
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 run | What changed | What 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 dashboard | Vote 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 confirmed | confirmed 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 ticks | Proof 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
| Level | Under TowerBFT | Under Alpenglow |
|---|---|---|
processed | Executed in a bank on this node; can still be dropped | Unchanged |
confirmed | Optimistic supermajority, about one slot out | The same state as finalized |
finalized | After 32 slots of vote lockout, about 12.8 s | A 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
confirmedis gone. - Don't test for
"confirmed"by equality. A signature can showfinalizedthe first time you look. Fresh signatures we looked up on October 28 already reportedfinalized. - Watch library defaults.
@solana/kitfills inconfirmedwhen a call names no commitment (source). Rust'sRpcClient::newdefaults tofinalized.
@solana/web3.js
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
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)
// 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:
| Message | Field | Notes |
|---|---|---|
SubscribeUpdateAccount | optional uint64 bank_id = 4 | Unset for startup snapshot accounts |
SubscribeUpdateSlot | optional uint64 bank_id = 5 | Unset for SLOT_FIRST_SHRED_RECEIVED, SLOT_COMPLETED and SLOT_DEAD |
SubscribeUpdateTransaction | uint64 bank_id = 3 | |
SubscribeUpdateTransactionStatus | uint64 bank_id = 6 | |
SubscribeUpdateBlock | uint64 bank_id = 14 | Added in yellowstone-grpc-proto 14.0.0 |
SubscribeUpdateBlockMeta | uint64 bank_id = 10 | |
SubscribeUpdateEntry | uint64 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
processedsubscriber can get two blocks for one slot, told apart bybank_id. Confirmed and Finalized are delivered once per slot, for the bank that reached them. SLOT_DEADcarries nobank_id. Drop every candidate for that slot. A repaired slot can come back with a newbank_id.bank_idis 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 theblockhashfrom 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:
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.
getRecentPerformanceSamplesshows it directly. On October 28,numTransactionsequalednumNonVoteTransactionsin every sample we fetched. For a series that spans the switch, plotnumNonVoteTransactions: 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: falsein 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_footerfilter with an opt-ininclude_certificates.
Checklist
- Move
confirmedreads and confirmations tofinalized, and name the commitment explicitly in Kit. - Accept
finalizedanywhere your code checks forconfirmed. - 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 onSLOT_DEAD. - Clear bank state on reconnect, and never compare
bank_idacross connections. - Re-baseline transaction counts and TPS; chart
numNonVoteTransactionsacross 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:10001and Amsterdam84.32.104.38:10001, plaintext HTTP/2. Connect withhttp://(orcreateInsecure()), neverhttps://, 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-tokenheader. See the gRPC docs. - Replay after a reconnect: set
from_slotto resume where you left off, as the sketch does. AllenHark keeps about 48 hours, so even a long outage can be replayed.SubscribeReplayInforeturnsfirst_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
- Shreds vs gRPC vs RPC vs WebSockets: which is best for low-latency bots?
- WebSocket streaming comes to AllenHark Relay, for send-only bots
- Understanding and minimizing Solana RPC latency
Sources
Checked October 28, 2026.
- Solana Foundation, Alpenglow: activation, genesis block, commitment,
bank_id, votes, ticks and Clock - SIMD-0326: Alpenglow and SIMD-0384: Alpenglow migration
- Agave CHANGELOG (4.3.0 Geyser changes) and votor/src/root_utils.rs
- Yellowstone gRPC geyser.proto and CHANGELOG
- Mainnet RPC (
getAgGenesisCert,getBlock,getSlot,getRecentPerformanceSamples) atapi.mainnet-beta.solana.com
