Solana Bot Latency: Where Your Trade Loses Time
Time every stage of a Solana trade, from the event to confirmation: how to measure each one and what to fix. With a Node.js harness for slots to land.
"My bot is slow" isn't something you can fix. A trade passes through eight stages between something happening on-chain and your own transaction being confirmed, and most of them can be timed. Once you know which stage eats the time, the fix is usually plain: move a server, change a feed, take an RPC call off the hot path, or send another way.
This post covers how to timestamp each stage, what makes it slow and what to change, with three small Node.js scripts: a harness that times a transaction end to end, a recorder that races two data feeds, and a helper that measures the round trip to an endpoint. There are no benchmark figures here, ours or anyone else's: measure them yourself, from the server your bot runs on.
Disclosure: AllenHark sells feeds, senders and servers for several of these stages. The method works the same with any provider.
The stages at a glance
| Stage | What to measure | Tool or API | If it is slow |
|---|---|---|---|
| 1. The event lands on-chain | The slot it landed in, and that slot's leader | The slot in your feed; getSlotLeaders | Nothing to fix: this is the starting line |
| 2. Your feed sees it | When each feed first delivers it, side by side | Shreds, Yellowstone gRPC, RPC WebSocket; the recorder | A faster feed type, a feed nearer your server, tighter filters, a read loop that only reads |
| 3. Decode and decide | Receipt to decision, in your process | process.hrtime.bigint() | No RPC calls on the hot path; state kept in memory |
| 4. Build and sign | Time to build and sign | The harness | Cached blockhash, prebuilt instructions, warm code |
| 5. Send | Send to acknowledgement; network round trip | The harness; the RTT helper | A server near the endpoint, kept-alive connections, no preflight |
| 6. Sender to leader | Not visible directly; read it from stage 7 and from drops | Slots to land, grouped by leader | A staked path, a relay, a priority fee, a Jito tip for bundles |
| 7. Inclusion | Landed slot minus the slot you sent at | getSignatureStatuses | Change the path or fee, then measure again |
| 8. Confirmation | Time until the status reads confirmed or finalized | getSignatureStatuses or signatureSubscribe | Subscribe or poll faster; wait on it only when you must |
Timestamps you can trust
On one machine, use a monotonic clock
Date.now() reads the wall clock, which your time daemon can step in the middle of a measurement. Use process.hrtime.bigint(), which Node documents as "not related to the time of day and therefore not subject to clock drift", or performance.now(), and take every timestamp of a run in one process.
Across machines, know your clock error
Timing from one machine to another is only as good as the clock sync. chrony states its bound as clock_error <= |system_time_offset| + root_dispersion + (0.5 * root_delay), and chronyc tracking prints all three terms; if the bound is bigger than the gap you want to measure, you are measuring noise. PTP with hardware support "is capable of sub-microsecond accuracy, which is far better than is normally obtainable with NTP", per Red Hat. Simpler still: measure round trips and races on one machine, with one clock.
On-chain, count slots, not seconds
getBlockTime, like the blockTime field on transactions, gives an "estimated production time, as Unix timestamp (seconds since the Unix epoch)": whole seconds, each about five slots long, and under Alpenglow the leader sets it itself, within a bound. It can't measure milliseconds.
Count slots instead. A slot is about 200 ms (we measured 210 to 216 ms on mainnet on 28 October 2026), and getSignatureStatuses returns the slot each transaction was processed in. getSlotLeaders names the leader of each slot, up to 5,000 slots per call, and getLeaderSchedule covers an epoch. Leaders take turns of four slots: 24 consecutive mainnet slots we read on 2 November 2026 came in runs of four per leader, so a turn lasts under a second.
The eight stages
1. The event lands on-chain
A pool is created, a wallet you copy buys, a price moves. The leader executes the transaction and sends its block out in pieces, called shreds, while it is still building it. You can't timestamp that moment from outside. Record the slot it landed in and that slot's leader, and measure everything else from there.
2. Your feed sees it
Feeds in the same place see the same transaction in this order:
- Shreds carry the block as the leader sends it out, before other nodes have replayed it.
- Yellowstone gRPC is a Geyser plugin inside an RPC node, which first has to receive the shreds and replay the block. Agave hands the plugin each batch of transactions as replay commits it, during the slot.
- RPC WebSockets come from the same kind of node, but Agave sends
processednotifications for accounts, logs and signatures a block at a time, once the node's commitment moves to that slot, encoded as JSON.
Distance can overturn that order: a shred feed on another continent can lose to a gRPC node in your rack. So run the feeds side by side in one process on your bot's server and record when each first delivers each signature, as the recorder below does. For a gRPC stream's lag and missed events, use the checks in why your Yellowstone stream lags. The fixes: the feed type, its location, a tighter filter, and a read loop that only reads and queues.
3. Decode and decide
Your code parses the update, applies your rules and decides. Put monotonic timestamps at receipt, after decoding and at the decision. This is local work, so it stays short until it touches the network: one RPC call on the hot path, for a pool account, a balance or a blockhash, costs at least a round trip and can outweigh all your local code put together. Keep state in memory, updated from your stream, and refresh the blockhash in the background (one lasts about 150 blocks, about 30 seconds).
4. Build and sign
Building and signing are CPU work in your process, and the harness times both. In our local runs, signing took longer than building, and the first run was slower than the rest at building, signing and sending: code that hadn't run yet, a connection that wasn't open yet. Prebuild what you can, cache the blockhash and lookup tables, and warm up before the first real trade.
5. Send: your machine to the RPC or sender
sendTransaction returns once the node takes the transaction in: Solana's docs say it "immediately succeeds, without waiting for any confirmations". So send-to-acknowledgement is your trip to the endpoint plus its intake, not landing. It grows with:
- Distance. The RTT helper measures the path from your server.
- New connections. A fresh HTTPS connection needs a TCP handshake, then a TLS handshake (one more round trip with TLS 1.3). Keep connections alive, or use the sender's persistent QUIC or WebSocket connection.
- Preflight. Unless you set
skipPreflight, the node verifies signatures and simulates the transaction first. If you've already checked it, skip preflight, as the harness does.
6. Sender to leader
Nobody outside the leader sees when your transaction reaches it, so this stage can't be timed directly. It shows up in two numbers you can measure: slots to land, and the share of transactions that never land.
What decides it: whether your path into the leader is staked (SWQoS), whether the leader runs a Jito client (Jito tips only count with Jito leaders), your priority fee, and how many paths your sender uses. SWQoS vs Jito vs a relay explains each. To improve it, change the sender or the fee, then measure again with the same transaction.
7. Inclusion: slots to land
Slots to land is the slot your transaction landed in, from getSignatureStatuses, minus the slot your RPC node was on when you sent it, which the harness keeps in memory from a slotSubscribe stream.
- +0: it landed in the slot your node was on when you sent.
- +1 or more: later slots. The harness names each landing slot's leader, so you can see whether slow landings cluster on certain leaders.
- Never: its blockhash expired first. Count these. A path that is quick when it lands but drops one transaction in ten is not fast.
The send slot is your node's view and can trail the leader, so treat a one-slot difference as approximate.
8. Confirmation
Under Alpenglow, confirmed and finalized describe the same state, and the Solana Foundation puts finality at roughly 150 ms. So the gap between "seen processed" and "confirmed" in the harness is mostly that, plus how often you ask: polling every 50 ms makes each reading up to 50 ms plus one round trip late, while signatureSubscribe pushes instead. Wait for confirmation only where your logic needs it, such as before selling what you just bought. Our Alpenglow guide covers the commitment changes.
Code: time a trade end to end
The harness sends RUNS small transactions, one at a time, and timestamps build, sign, send to acknowledgement, first seen at processed, and confirmed. It records your node's slot at send time and the slot each transaction landed in, names each landing slot's leader, and prints p50, p90, p99 and max per stage, with the spread of slots to land. Failures and expiries are counted, not left out.
// measure-trade.mjs: time each stage of a Solana transaction, many times over,
// and print percentiles and slots to land.
// npm i @solana/web3.js@1 (Node 18 or later)
//
// RPC_URL your RPC endpoint; its WebSocket (or WS_URL) is used for slot updates
// KEYPAIR path to a keypair JSON file holding a little SOL for fees
// SEND_URL optional: any endpoint that takes JSON-RPC sendTransaction (default: RPC_URL)
// TIP_ACCOUNT optional, with TIP_LAMPORTS: a tip transfer, if your sender wants one
// CU_PRICE priority fee in micro-lamports per compute unit: use what your bot pays
// RUNS transactions to send (default 20)
// GAP_MS pause between runs (default 2000), so runs spread over slots and leaders
// POLL_MS how often to ask for the transaction's status (default 50)
import fs from "node:fs";
import {
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
SystemProgram,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
const env = process.env;
if (!env.RPC_URL || !env.KEYPAIR) throw new Error("Set RPC_URL and KEYPAIR");
const RUNS = Number(env.RUNS ?? 20);
const GAP_MS = Number(env.GAP_MS ?? 2000);
const POLL_MS = Number(env.POLL_MS ?? 50);
const CU_PRICE = Number(env.CU_PRICE ?? 0);
const TIP = env.TIP_ACCOUNT ? { to: new PublicKey(env.TIP_ACCOUNT), lamports: Number(env.TIP_LAMPORTS) } : null;
const payer = Keypair.fromSecretKey(Uint8Array.from(JSON.parse(fs.readFileSync(env.KEYPAIR, "utf8"))));
const rpc = new Connection(env.RPC_URL, { commitment: "finalized", wsEndpoint: env.WS_URL });
const sender = env.SEND_URL ? new Connection(env.SEND_URL, "finalized") : rpc;
const now = () => process.hrtime.bigint(); // monotonic nanoseconds: immune to clock changes
const ms = (from, to) => (to === null ? null : Number(to - from) / 1e6);
const sleep = (t) => new Promise((r) => setTimeout(r, t));
// The newest slot this RPC node has reported, kept in memory by a subscription,
// so reading it at send time costs nothing.
let nodeSlot = 0;
rpc.onSlotChange(({ slot }) => (nodeSlot = Math.max(nodeSlot, slot)));
const runs = [];
for (let i = 0; i < RUNS; i++) {
if (i > 0) await sleep(GAP_MS);
// Off the clock: a real bot keeps a fresh blockhash and the slot in memory.
const { blockhash, lastValidBlockHeight } = await rpc.getLatestBlockhash("finalized");
nodeSlot = Math.max(nodeSlot, await rpc.getSlot("processed"));
const t0 = now(); // the decision is made: build
const instructions = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_000 }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: CU_PRICE }),
// Stand-in for your trade: move i + 1 lamports to yourself, so each run has its own signature.
SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: payer.publicKey, lamports: i + 1 }),
];
if (TIP) instructions.push(SystemProgram.transfer({ fromPubkey: payer.publicKey, toPubkey: TIP.to, lamports: TIP.lamports }));
const tx = new VersionedTransaction(
new TransactionMessage({ payerKey: payer.publicKey, recentBlockhash: blockhash, instructions }).compileToV0Message(),
);
const t1 = now();
tx.sign([payer]);
const t2 = now();
const sendSlot = nodeSlot;
let signature;
try {
signature = await sender.sendRawTransaction(tx.serialize(), { skipPreflight: true });
} catch (e) {
runs.push({ i, sendError: e.message });
continue;
}
const t3 = now(); // the endpoint said it accepted the transaction: not that it landed
// Poll until the status shows processed, then confirmed or finalized,
// or until the blockhash expires. Each time is up to POLL_MS + one round trip late.
let seen = null, done = null, slot = null, err = null;
for (let polls = 1; done === null; polls++) {
const [st] = (await rpc.getSignatureStatuses([signature])).value;
if (st) {
seen ??= now();
slot = st.slot;
err = st.err;
if (st.confirmationStatus === "confirmed" || st.confirmationStatus === "finalized") done = now();
} else if (polls % 20 === 0 && (await rpc.getBlockHeight("finalized")) > lastValidBlockHeight) {
break; // expired: it will never land
}
if (done === null) await sleep(POLL_MS);
}
runs.push({
i, signature, sendSlot, slot, err, landed: done !== null,
build: ms(t0, t1), sign: ms(t1, t2), ack: ms(t2, t3), seen: ms(t2, seen), confirmed: ms(t2, done),
});
}
// Off the clock: who was leader in each landed slot (one call, up to 5,000 slots).
const landed = runs.filter((r) => r.landed);
const leaders = new Map();
if (landed.length) {
const first = Math.min(...landed.map((r) => r.slot));
const last = Math.max(...landed.map((r) => r.slot));
if (last - first < 5000) {
(await rpc.getSlotLeaders(first, last - first + 1)).forEach((pk, k) => leaders.set(first + k, pk.toBase58()));
}
}
const f = (x) => (x === null ? "-" : x.toFixed(3));
console.log("run build sign send→ack send→seen send→conf send slot landed slots leader");
for (const r of runs) {
if (r.sendError) { console.log(`${String(r.i + 1).padStart(3)} send failed: ${r.sendError}`); continue; }
const delta = r.landed ? `+${r.slot - r.sendSlot}` : "-";
console.log(
`${String(r.i + 1).padStart(3)} ${f(r.build).padStart(7)} ${f(r.sign).padStart(7)} ${f(r.ack).padStart(9)}` +
` ${f(r.seen).padStart(10)} ${f(r.confirmed).padStart(10)} ${String(r.sendSlot).padStart(10)}` +
` ${String(r.slot ?? "-").padStart(7)} ${delta.padStart(6)} ${(leaders.get(r.slot) ?? "-").slice(0, 8)}` +
(r.err ? ` failed: ${JSON.stringify(r.err)}` : ""),
);
}
// Nearest-rank percentile. With fewer than 100 runs, p99 is simply the slowest run.
const pct = (xs, p) => [...xs].sort((a, b) => a - b)[Math.max(0, Math.ceil((p / 100) * xs.length) - 1)];
const sent = runs.filter((r) => !r.sendError);
console.log(
`\n${runs.length} runs: ${landed.length} landed (${landed.filter((r) => r.err).length} failed on-chain),` +
` ${sent.length - landed.length} expired without landing, ${runs.length - sent.length} send errors`,
);
if (sent.length) console.log("stage (ms)".padEnd(22) + ["p50", "p90", "p99", "max"].map((h) => h.padStart(10)).join(""));
for (const [label, key, rows] of [
["build", "build", sent], ["sign", "sign", sent], ["send → ack", "ack", sent],
["send → seen processed", "seen", landed], ["send → confirmed", "confirmed", landed],
]) {
const xs = rows.map((r) => r[key]).filter((x) => x !== null);
if (!xs.length) continue;
console.log(label.padEnd(22) + [50, 90, 99, 100].map((p) => f(pct(xs, p)).padStart(10)).join(""));
}
if (landed.length) {
const deltas = landed.map((r) => r.slot - r.sendSlot);
const counts = {};
for (const d of deltas) counts[d] = (counts[d] ?? 0) + 1;
console.log(
`slots to land: p50 +${pct(deltas, 50)}, p90 +${pct(deltas, 90)}, max +${pct(deltas, 100)} (` +
Object.entries(counts).map(([d, n]) => `+${d}: ${n}`).join(", ") + ")",
);
}
process.exit(0);Run it from the server your bot runs on, against your own RPC:
npm i @solana/web3.js@1
export RPC_URL="https://your-rpc-endpoint"
export KEYPAIR="$HOME/keys/latency-test.json" # a test wallet holding a little SOL
RUNS=100 node measure-trade.mjsEach run sends a real transaction and pays its fee, plus any tip you set, so use a test wallet. While it waits, the harness polls about 20 times a second, too often for a rate-limited public endpoint. Set CU_PRICE to the fee your bot really pays, or you are measuring a different transaction from the one you trade with.
This is the output format, from a local solana-test-validator on one machine. These numbers say nothing about mainnet: a test validator has one leader, no network in between and no competition.
run build sign send→ack send→seen send→conf send slot landed slots leader
1 1.486 1.770 1.209 364.767 466.869 3263 3264 +1 H2T81tit
2 0.440 1.096 0.700 310.666 362.441 3269 3269 +0 H2T81tit
3 0.328 0.986 0.641 310.447 361.859 3274 3274 +0 H2T81tit
...
20 runs: 20 landed (0 failed on-chain), 0 expired without landing, 0 send errors
stage (ms) p50 p90 p99 max
build 0.204 0.398 1.486 1.486
sign 0.966 1.197 1.770 1.770
send → ack 0.697 0.918 1.209 1.209
send → seen processed 362.850 414.918 416.624 416.624
send → confirmed 363.558 466.869 468.881 468.881
slots to land: p50 +0, p90 +0, max +1 (+0: 19, +1: 1)Compare send → ack with the RTT helper's figure for the same host: if the acknowledgement takes far longer than the round trip, the time is going in the endpoint or in connection setup. send → seen processed covers the sender, the leader, the slot and your polling. The slots and leader columns show where slow landings happen. Run 1 is a warm-up; in small samples, leave it out.
To time another sender, point SEND_URL at any endpoint that takes JSON-RPC sendTransaction, adding TIP_ACCOUNT and TIP_LAMPORTS if it wants a tip. For a sender with its own API, replace the sendRawTransaction line with its call; our sender comparison shows the call for AllenHark Relay.
How we tested it: we ran it unchanged against solana-test-validator and checked every branch: a tip (it reached the tip account), a refused connection (counted as a send error), a mock endpoint that accepted transactions and dropped them (counted as expired once the blockhash ran out), and a transfer larger than the wallet's balance (landed, counted as failed on-chain). Nothing was sent to mainnet or devnet.
Code: compare two feeds
To find out which feed sees transactions first from your server, call mark() from every feed's handler, in one process, as soon as you have the signature, then call report() after a few minutes.
// first-seen.mjs: which of two feeds delivers each transaction first, and by how much?
// Call mark() from every feed's handler, in the same process, as soon as you
// have the signature. One clock, so the differences are exact for this machine.
const seen = new Map(); // signature -> { [feed]: time in ns }
export function mark(feed, signature) {
const t = process.hrtime.bigint();
const row = seen.get(signature) ?? {};
row[feed] ??= t; // keep the first delivery only
seen.set(signature, row);
}
export function report(a, b) {
const lead = []; // ms by which feed a beat feed b (negative: b was first)
let onlyA = 0, onlyB = 0;
for (const row of seen.values()) {
if (row[a] !== undefined && row[b] !== undefined) lead.push(Number(row[b] - row[a]) / 1e6);
else if (row[a] !== undefined) onlyA++;
else onlyB++;
}
if (!lead.length) return console.log("no signature seen by both feeds yet");
lead.sort((x, y) => x - y);
const at = (p) => lead[Math.max(0, Math.ceil((p / 100) * lead.length) - 1)].toFixed(3);
const first = lead.filter((x) => x > 0).length;
console.log(`${lead.length} seen by both: ${a} first ${first} times, ${b} first ${lead.length - first} times`);
console.log(`${a}'s lead in ms: p10 ${at(10)} p50 ${at(50)} p90 ${at(90)}`);
console.log(`seen only by ${a}: ${onlyA}, only by ${b}: ${onlyB} (check these against RPC)`);
}Give both feeds the same filter, such as one program or one pool, or the "seen only by" counts will mostly measure the filters. Check a few of those signatures against getSignaturesForAddress. We tested the recorder with two WebSocket log subscriptions on a local test validator, one at processed and one at confirmed: the processed feed came first for all 15 transactions, as it should.
Code: how far is an endpoint?
A TCP connect completes after one round trip, so timing connects measures the network path to a host without the service's own processing. Give it the host and port of your RPC, feed or sender:
// tcp-rtt.mjs: how far is an endpoint from this machine? Times TCP connects.
// A TCP connect completes after one round trip (SYN out, SYN-ACK back), so the
// connect time is close to the network round-trip time to that host.
// Usage: node tcp-rtt.mjs host:port [count]
import dns from "node:dns/promises";
import net from "node:net";
const [target, countArg] = process.argv.slice(2);
const cut = target ? target.lastIndexOf(":") : -1;
if (cut < 1) {
console.error("usage: node tcp-rtt.mjs host:port [count]");
process.exit(1);
}
const host = target.slice(0, cut).replace(/^\[|\]$/g, ""); // allows [::1]:8899
const port = Number(target.slice(cut + 1));
const count = Number(countArg ?? 20);
const { address } = await dns.lookup(host); // resolve once, so DNS time is not counted
function connectOnce() {
return new Promise((resolve, reject) => {
const start = process.hrtime.bigint();
const socket = net.connect({ host: address, port });
socket.setTimeout(3000, () => {
socket.destroy();
reject(new Error("timed out"));
});
socket.once("error", reject);
socket.once("connect", () => {
const took = Number(process.hrtime.bigint() - start) / 1e6;
socket.destroy();
resolve(took);
});
});
}
const times = [];
for (let n = 1; n <= count; n++) {
try {
times.push(await connectOnce());
} catch (e) {
console.log(`connect ${n}: ${e.message}`);
}
await new Promise((r) => setTimeout(r, 200)); // be polite: a few connects a second
}
if (!times.length) process.exit(1);
times.sort((a, b) => a - b);
const at = (p) => times[Math.max(0, Math.ceil((p / 100) * times.length) - 1)].toFixed(3);
console.log(`${host} (${address}) port ${port}: ${times.length}/${count} connects`);
console.log(`min ${at(0)} ms p50 ${at(50)} ms p90 ${at(90)} ms max ${at(100)} ms`);node tcp-rtt.mjs your-rpc-host.example:443 20The minimum is the path; the spread above it is queueing and jitter. QUIC runs over UDP, so point the helper at a TCP port on the same host, such as its HTTPS port. Behind a load balancer or an anycast address, you time the nearest edge, not necessarily the machine that handles your request. And keep the count low: it is someone else's server.
We ran it only against a local port: 10 connects to the test validator's RPC port on the same machine gave min 0.363 ms p50 0.409 ms p90 0.471 ms max 1.451 ms.
Measuring fairly
Before you believe a result, yours or a vendor's:
- Same transaction. Same instructions, accounts, compute limit and priority fee. A swap on a busy pool and a transfer to yourself are different tests.
- Many samples, percentiles. Report p50, p90 and p99, not an average: one stuck transaction drags an average around, and the slow tail is where trades are lost. p99 needs at least 100 runs to mean anything.
- Count every failure. Send errors, on-chain failures and expiries all go in the totals. A path that lands 90 transactions out of 100 can't be ranked against one that lands all 100 on speed alone.
- Same time, same transaction. Conditions and leaders change by the minute. Best: send the same trade through each sender at once as durable-nonce variants, so at most one copy lands and you see which sender got there first; mind the caveats in that post. Next best: short sessions in turn, A then B then A, not one long session each.
- Many leaders. With turns of four slots, a short test meets few leaders. Spread runs over hours and group results by leader.
- From your server, more than once. A test from a laptop at home measures your home connection, and a single run proves nothing.
Where AllenHark fits
What we sell for each stage. Don't take our word for it: run the scripts above against us, from your server, as you would against anyone else.
- Feed: ShredStream. Four feeds of the data validators exchange while a block is produced: decoded entries over gRPC or UDP, raw shreds over UDP, or Jito's proxy protocol, from Frankfurt, with Amsterdam for some plans. The shared stream and Direct UDP each have a free hour, started from the console: long enough to race one against your current feed with the recorder.
- Feed: Yellowstone gRPC. Streams at
processedwith Alpenglow'sbank_idlive, afrom_slotreplay of about 48 hours, and auth by allowlisted IP or anx-tokenAPI key. See the gRPC docs. - Send: AllenHark Relay. Formerly 0-slot. It lands transactions in the same block, at 0.1 ms over QUIC, and takes Jito bundles. Each API key gets 10 requests per second by default, more on request. The relay docs recommend co-locating with a relay region for the 0.1 ms figure.
- Send: Slipstream. A separate product from Relay: a sender-agnostic relay that follows the leader schedule and routes each transaction, bundle or TPU submission through the worker nearest the current leader, with 0.2 ms leader RTT. Its SDK probes the available regions, ranks them by latency and connects over QUIC, falling back to gRPC, WebSocket and HTTP. See the Slipstream docs.
- Location: co-location. Single-tenant AMD bare metal in Frankfurt, Amsterdam and Chicago, and cloud VPS on our own hardware in Frankfurt, paid monthly. Run the RTT helper before and after you move.
- RPC: AllenHark RPC. 100 requests per second on the standard plan, enough for the harness's polling. Dedicated RPC and Dedicated gRPC have no rate limits.
- All of it, managed: AllenHark bots. They run on your own server, read the AllenHark market feed over QUIC and detect in 0.2 ms, across every bot.
FAQ
How many slots should a Solana transaction take to land?
There is no single number: it depends on your path to the leader, the leader, your fee and how busy the cluster is. Measure your own distribution of landed slot minus send slot, with your real transaction and fee, and count the transactions that never land.
Can I measure Solana latency with blockTime?
No. blockTime is a Unix timestamp in whole seconds, and under Alpenglow the leader sets it within a bound. Use slots for distance on the chain and a monotonic clock in your own process for everything else.
Does the sendTransaction response mean my transaction landed?
No. It means the RPC node or sender accepted it. It has landed when getSignatureStatuses first returns a status for it, and the slot in that status is where it landed.
How do I compare two senders fairly?
Send the same transaction through both at once, as durable-nonce variants so only one copy can land, over many runs and leaders. Count failures and expiries, and compare percentiles, not averages.
Related reading
- SWQoS vs Jito vs a relay: which Solana sender to use, with durable-nonce variants for testing senders side by side.
- Shreds vs gRPC vs RPC vs WebSockets and what Solana shreds are.
- Why your Yellowstone gRPC stream lags or misses events, with a stream lag check.
- Detect new Pump.fun tokens and PumpSwap and Raydium pools in real time.
- Alpenglow: what Solana RPC and gRPC users need to change.
- Why Solana transactions arrive late even with priority fees and the fastest way to land Solana transactions.
Sources
Checked November 2, 2026.
- Solana RPC docs: getSignatureStatuses (the
slotandconfirmationStatusfields), sendTransaction (acceptance is not confirmation; preflight), getBlockTime (seconds since the Unix epoch), getSlotLeaders (up to 5,000 slots), getLeaderSchedule and slotSubscribe. - Solana Foundation, Alpenglow: finality of roughly 150 ms,
confirmedandfinalizeddescribing the same state, the block timestamp set by the leader. - Agave v4.4.0 source: transaction_status_service.rs (Geyser gets each committed batch), rpc_subscriptions.rs and commitment_service.rs (WebSocket notifications per block, when the commitment moves).
- Node.js docs: process.hrtime.bigint(), performance.now(), net.
- chrony, chronyc manual (
trackingand the clock error bound); Red Hat, Configuring PTP using ptp4l. - IETF, RFC 9293 (TCP's three-way handshake) and RFC 8446 (the TLS 1.3 handshake).
- Our reads of mainnet's public RPC on 2 November 2026 (
getSlotLeaders,getBlockTime), and slot times we measured on mainnet RPC on 28 October 2026. - AllenHark: ShredStream and its docs, Yellowstone gRPC, Relay and its docs, Slipstream and its docs, co-location, RPC, bots.
