---
title:    Inside the Block: how Solana validators order transactions
author:   Maksym Sushchuk (NAVISTRI)
date:     2026-11-04
window:   Solana mainnet, 1 to 3 November 2026
repo:     https://github.com/souschuk/inside-the-block
video:    https://youtu.be/Qy4snotpZkc
---

# Inside the Block

How ten Solana validators on eight clients order the transactions in their blocks. I
measured it on mainnet after Alpenglow, 1 to 3 November 2026, with 1,263 scored pairs of my
own transactions.

Video (4:30): [youtu.be/Qy4snotpZkc](https://youtu.be/Qy4snotpZkc)

## answer

It depends on the moment. When the leader held both transactions, all ten validators put the
one with more reward per unit of cost first in every pair: 215 of 215 in one contrast, 211
of 211 in the other. Once the block was being built, Agave, Jito Labs and Rakurai took them
in the order they arrived, and the better one got ahead only 4 times in 119. Firedancer, BAM
and Harmonic work in batches of tens of milliseconds, which is most likely why public tests
disagree. You can check every pair on chain with getBlock.

## question: The question

When two transactions compete for a place in a Solana block, does money decide, or whoever
arrives first? Public tests disagree, so I measured it myself on mainnet. The answer turned
out to depend on when they reach the leader.

## block: How a block gets built

Solana runs in slots of about 200 milliseconds. Every slot has one leader, the validator
that builds its block, and a leader gets four slots in a row. A lottery weighted by stake
sets the leader schedule a day ahead. There's no shared mempool. You send a transaction
straight to the leader, or an RPC node sends it for you.

After Alpenglow:

Alpenglow went live on mainnet on 21 October 2026. A leader still holds four slots in a row,
and transactions reach it the same way as before. Every measurement here comes after the
upgrade, and the program refuses to send before it.

## reward: Reward and cost

The leader earns the priority fee plus half of the signature fee. That's its reward. Every
transaction also has a cost, counted from its compute, signatures, writes and data. Agave,
the code most validators are built on, sorts transactions by reward per unit of cost. But is
that what validators really do?

The rule in the code:

Agave and Jito Labs rank a transaction by reward × 1,000,000 ÷ (cost + 1)
([transaction_priority.rs](https://github.com/anza-xyz/agave/blob/v4.3.2/core/src/transaction_priority.rs#L32-L66)).
The reward is the priority fee plus the half of the signature fees that is not burned. The
cost counts the requested compute limit, 720 units per signature, 300 per write lock, one
per 4 bytes of instruction data and 8 per 32 KiB of requested loaded data
([block_cost_limits.rs](https://github.com/anza-xyz/agave/blob/v4.3.2/cost-model/src/block_cost_limits.rs#L8-L20)).

Firedancer takes the same reward and compares two transactions by cross-multiplying reward
and cost, without the + 1
([fd_pack.c](https://github.com/firedancer-io/firedancer/blob/v26.10.5/src/disco/pack/fd_pack.c#L1040-L1063)).
BAM, Harmonic and Rakurai publish no sort, so there the Agave rule was the hypothesis under
test.

## pair: The pair

So I sent pairs: two of my own transactions to the same leader, both writing to the same
fresh account, so one has to go first. And I priced them so the rules disagree about the
winner. In the first pair, Y pays more, but it also costs more. In the second, D pays more
than A for each unit of compute, but it asks to load the maximum amount of account data. So
per unit of cost, D pays five times less. Only reward per unit of cost puts Y first in one
pair and A first in the other.

Which rule puts which transaction first:

| pair            | reward ÷ cost | CU price | lamport fee | cost alone | no rule |
|-----------------|---------------|----------|-------------|------------|---------|
| C4: Y against A | Y             | Y        | Y           | A          | either  |
| C6: D against A | A             | D        | D           | A          | either  |

Only reward per unit of cost puts Y first in C4 and A first in C6. Both members write to one
fresh account, so one has to wait for the other. Each has its own fee payer, so neither can
hold the other back.

## moments: Two moments

Timing matters too. Some pairs reached the leader about a second before its slot, so both
waited in its buffer. For those, a hash picked which one I sent first. Other pairs arrived
while the block was already being built, and I sent the better one 2, 10 or 40 milliseconds
after the other.

How the timing works:

A buffered pair leaves when the program sees the slot five before the leader's first one,
about a second before the leader's first shred, so both members wait in its buffer and its
sort sees them together. A window pair leaves when the program sees the leader's first slot,
so both reach a leader that is already producing.

Which payer goes first is the parity of one byte of a hash seeded by the frozen plan, so the
send order is random and anyone can recompute it. Each window gets its own QUIC connection,
opened about 7 s before the pair, and both members travel on it.

## calibration: Calibration

Before I trusted the method, I tried it on a question with a known answer. Since version
4.4, Agave breaks ties by arrival order. Version 4.3 doesn't. And that's exactly what I
measured: 90 out of 90, against 46 out of 90.

The known answer:

From v4.4.0 on, Agave keeps arrival order among equal priorities
([#14836](https://github.com/anza-xyz/agave/pull/14836)). Block T sent identical pairs to
validators on both sides of that change. The treated validators put the first sent first in
90 of 90 pairs, and the controls on 4.3.2 in 46 of 90, which is what a coin gives.

## frozen: Frozen on chain

I froze every plan in advance. Before each block of pairs, the hashes of the plan, the code
and the target list went on chain in a Memo. If anything changes, the program refuses to
send.

Where the Memos are:

Block T's Memo is in slot 464,002,340 and its first pair in slot 464,018,360. Block P's Memo
is in slot 464,169,852 and its first pair in slot 464,170,012. `python tools/reproduce.py
--chain` recomputes the hashes from the files in the repository and reads them back from
both Memos.

## kyiv: From Kyiv

It all ran from a battery-backed home server in Kyiv, where planning around Solana's leader
schedule turned out easier than planning around the blackout schedule. Three sessions in 48
hours, nearly 1,400 pairs.

The path:

One host sent over QUIC straight to the TPU address each leader advertises, with no RPC
node, no staked relay and no Jito. The sender holds no stake and stays under the limits for
unstaked connections. If the slot clock was late or stale, the pair was cancelled rather
than sent late, and the reason went into the data.

## held: When the leader holds both

When the leader holds both transactions at once, the answer is clean. All ten validators,
running eight different clients, sort them by reward per unit of cost. Y went first in all
215 pairs, and D in none of its 211.

## window: When the block is being built

Once the block is being built, the picture splits. Agave, Jito Labs and Rakurai take
transactions in the order they arrive. The better one got ahead only 4 times out of 119.
Firedancer, BAM and Harmonic work in batches of tens of milliseconds. That's most likely why
public tests disagree: a transaction sent through an RPC node often reaches the leader in
the middle of a block.

The curve for each validator:

Sent 2 to 40 ms after the other into a running block, the better transaction overtook 4
times in 119 on Jito Labs, Agave and Rakurai. On Firedancer it overtook every time at 2 and
10 ms and never at 40 ms. BAM and Harmonic fall in between. `tools/horizon.py` fits a batch
period to each curve.

Figure: [curve.svg](https://navistri.com/reports/inside-the-block/curve.svg). For each
validator, the share of pairs in which the better transaction landed first: when the leader
held both, and when it was sent 2, 10 or 40 ms after the other while the leader was
producing. Jito Labs, Agave and Rakurai drop to near zero in the window; Firedancer stays at
the top to 10 ms and drops at 40 ms; BAM and Harmonic decline more slowly.

## results: All ten validators

Every validator ranked my transactions by reward per unit of cost, strictly, when it held
both. They differ in the last two columns.

| validator | client                     | equal priority  | inside the block | pairs   |
|-----------|----------------------------|-----------------|------------------|---------|
| 3SkE34PV  | JitoLabs 4.3.2             | no relation     | arrival order    | 138/144 |
| HEL1USMZ  | JitoLabs 4.4.0             | arrival order   | arrival order    | 143/144 |
| 7QQGNm3p  | Agave 4.3.2                | no relation     | arrival order    | 136/144 |
| ana2y2Yv  | Rakurai 4.3.2              | no relation     | arrival order    | 96/96   |
| 9eGrDohd  | Firedancer 26.10.5         | reverse arrival | batch ~40 ms     | 144/144 |
| 5Cchr1XG  | AgaveBam 4.3.2             | no relation     | batch ~45 ms     | 128/144 |
| DRpbCBMx  | AgaveBam 4.4.0             | reverse arrival | batch ~50 ms     | 126/144 |
| 9jxgosAf  | FireBAM 26.10.5            | no relation     | batch >78 ms     | 129/144 |
| Fd7btgyS  | HarmonicAgave 4.4.0        | no relation     | batch ~45 ms     | 117/144 |
| JUPiTERr  | HarmonicFiredancer 26.10.5 | no relation     | batch ~36 ms     | 106/144 |

On the two Harmonic validators one probe kind went the other way in 7 of 7 pairs, so the key
there is close to Agave's and not the same. Client labels are what each validator reports in gossip. ana2y2Yv
sat out the third session after it moved to Rakurai 4.4.0. Counts, intervals and sensitivity
views are in [REPORT.md](https://github.com/souschuk/inside-the-block/blob/main/REPORT.md).

## limits: Limits

I didn't measure the exact rule on Harmonic, the RPC path itself, or heavy transactions that
fight over busy accounts. The repo lists every limit.

Read this before quoting a number:

- A verdict describes the path measured: one unstaked sender writing straight to the
  leader's TPU, or to the BAM or Harmonic ingress in front of it.
- The pairs are small, untipped and touch only fresh accounts. Bundles, heavy transactions
  and contended accounts are outside the study.
- A verdict is about one validator in the sessions it ran in, not about every validator on
  its client.
- No pair went through an RPC node. What this page says about measurements that do comes
  from the window pairs and their timing, not from a measurement.

## check: Check it yourself

You don't have to trust me. One command rebuilds every number from the raw data, offline and
without keys, and you can check every pair on chain with getBlock.

Rebuild every number:

```bash
git clone https://github.com/souschuk/inside-the-block && cd inside-the-block && docker compose up --build
```

It runs the tests, then recomputes the reports for both blocks from data/ and compares them
with the published ones.

Look up the first pair of block P:

```bash
curl -s https://api.mainnet-beta.solana.com -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":1,"method":"getBlock","params":[464170012,{"encoding":"json","transactionDetails":"signatures","rewards":false,"maxSupportedTransactionVersion":1}]}'
```

Its two signatures sit at indexes 10 and 11 of the 359 in result.signatures.

Repository: [github.com/souschuk/inside-the-block](https://github.com/souschuk/inside-the-block)

Questions about the method or the data: [max@sushch.uk](mailto:max@sushch.uk).

<!-- one unstaked sender on a home battery, and every leader still showed its rule. -->
