The Throughput Problem: Why Serialized Execution Caps a Chain
The overview laid out what you are going to build: a small runtime, solmini, that models the two ideas that make Solana Solana. This page is the why. Before we write a clock or a scheduler, we need to see the exact wall a straightforward “world computer” runs into — and why that wall is a single CPU core.
The throughline of the whole book is one question: how do you build a single global state machine that runs at hardware speed without falling apart? This page answers the first half — “at hardware speed” — by finding the one design decision that keeps a naive chain slow, and the one change that removes it. Everything else in the part (a verifiable clock, a parallel scheduler, a commit-and-rollback runtime) is downstream of the insight on this page.
A world computer that runs one transaction at a time
Section titled “A world computer that runs one transaction at a time”Start from the simplest possible design for a programmable chain. There is one global state — a big map from addresses to data — and a stream of transactions that each invoke some contract. To stay deterministic (every validator must compute the same new state), you pick an order for the transactions and apply them one after another:
global state S0 │ apply tx0 → S1 │ apply tx1 → S2 │ apply tx2 → S3 ▼ ... final state SnThis is exactly how the Ethereum Virtual Machine executes a block: transactions run strictly in sequence, each seeing the state the previous one left behind. It is obviously correct. It is also, just as obviously, stuck on one core. You have a 32- or 64-core machine, and 63 of those cores sit idle while the one core grinds through the block. The chain’s throughput is the throughput of a single thread — and single-thread speed stopped doubling years ago.
The natural reaction is: why not run the transactions on all the cores at once? Hold that thought. The reason you can’t — in this design — is the whole point.
Why you can’t just parallelize it: hidden state
Section titled “Why you can’t just parallelize it: hidden state”Two transactions can run at the same time safely only if they don’t touch the same data in a conflicting way. If tx0 and tx1 never write the same slot, the order between them doesn’t matter and you can run them on separate cores. So the question becomes: can the runtime tell, before it runs anything, whether two transactions conflict?
In the naive world-computer, the answer is no — and the reason is architectural. A contract holds its own storage inside itself. The state a call will read or write is decided by the code as it executes: a branch, a loop, a computed storage key, a call into another contract. You cannot know the footprint of tx0 until you have run tx0.
Ethereum-style contract what the runtime sees ───────────────────────── ───────────────────── contract Vault { tx: "call Vault.withdraw(...)" mapping storage; ← hidden footprint: ??? unknown until it runs fn withdraw() { // touches which slots? // depends on inputs, branches, // and cross-contract calls } }That uncertainty is fatal for parallelism. To discover that tx0 and tx1 conflict, you would have to execute them — but executing them is the very thing you were trying to schedule. And if you guessed “they’re independent” and ran them together, two transactions might both write the same balance and produce a state that depends on which core finished first. That is a data race, and on a blockchain a data race means two validators compute two different “correct” states and the network forks.
So the sequential rule is not laziness. A runtime is forced to serialize whenever a transaction’s footprint is unknown until it runs. Hidden state ⟹ unknown footprint ⟹ mandatory serial execution ⟹ one-core throughput. Follow that chain of implications and you have found the real bottleneck. It isn’t the VM being slow; it’s the information the VM is missing.
The core insight: declare your footprint up front
Section titled “The core insight: declare your footprint up front”Here is the move that breaks the chain of implications. What if every transaction had to declare, up front, the exact set of accounts it will read and the set it will write — before the runtime executes a single instruction?
Then the runtime knows every transaction’s footprint without running it. Conflict detection becomes a cheap set comparison instead of an execution. Two transactions conflict when they share an account and at least one of them writes it. The runtime can look at a batch of pending transactions, compute which ones are pairwise independent, and hand those to different cores with total confidence that they cannot collide.
txs (submission order): t0 t1 t2 t3 t4 accounts they write: A B A C B
batch 0 (parallel): t0[A] t1[B] t3[C] ← disjoint, run together batch 1 (parallel): t2[A] t4[B] ← each waited on an earlier writerNothing about the work changed — the same programs run, touching the same accounts. What changed is when the runtime learns the footprint: up front, declaratively, instead of by executing. That single shift — from hidden, discovered-at-runtime state to declared, known-in-advance state — is what turns a one-core chain into a many-core one.
This is why the account model (the next major piece you’ll build) matters so much. In Solana, programs are stateless code and all state lives in separate, named accounts that a transaction lists explicitly. The declaration isn’t a bolt-on convenience; it’s forced by the data model. We build that model in Accounts and Stateless Programs, and the scheduler that exploits it in The Account-Locks Scheduler.
Under the hood — declaration lifts Rust’s borrow rule to accounts
Section titled “Under the hood — declaration lifts Rust’s borrow rule to accounts”If the conflict rule (“any number of readers, or exactly one writer”) sounds familiar, it should: it is Rust’s shared-XOR-mutable rule, the one the borrow checker enforces on every &T and &mut T. Rust’s guarantee is that at any instant a value has either many shared references or one exclusive mutable reference, never both — which is precisely what makes a data race unrepresentable at compile time.
Rust, on variables: Solana runtime, on accounts: ─────────────────── ──────────────────────────── &T → many readers OK ↔ read-only account → many txs may share it &mut T → exactly one ↔ writable account → exactly one tx at a time the compiler rejects the scheduler serializes shared + mutable conflicting transactionsThe account footprint declaration is what lets you lift that rule from a single program’s variables up to the whole chain’s global state. Each transaction’s declared writable set is its &mut; its read-only set is its &. A conflict-free batch is a set of borrows the checker would accept. This is the conceptual through-line of the entire part — and it is why we build the runtime in Rust: when the scheduler finally hands a batch to a thread pool, the language makes the underlying data race unrepresentable, so “run these transactions on different cores” is safe by construction rather than by hope. You’ll see this concretely: each transaction runs against its own owned clone of its accounts, and because the batch is conflict-free, no two clones ever share a writable account.
The design question Solana asked
Section titled “The design question Solana asked”Every serious system has one number its designers refused to compromise. For Solana that number is transactions per second per dollar of fees — raw throughput at the lowest possible cost, on a single synchronous chain. The thesis, from the project’s origins around 2017–2018, was that a blockchain should feel like the internet: fast enough that confirmation feels instant, cheap enough that a transaction costs a rounding error.
Taking that goal seriously turns “make it fast” into two concrete engineering problems, and Solana answered each with a named mechanism you will build:
- Agreeing on order is expensive. If validators must exchange messages to agree on when each transaction happened, that conversation is the bottleneck. Proof of History (PoH) removes the conversation with a hash chain that is a verifiable clock — every validator checks the ordering independently. You build it in Proof of History.
- Executing transactions serially wastes the machine. This page’s problem. Sealevel — the declare-your-footprint scheduler — runs the non-conflicting transactions in parallel across every core. You build it in The Account-Locks Scheduler.
And behind both is a hardware bet: rather than keep the base layer conservative and push activity onto layer-2 systems (Bitcoin’s and Ethereum’s path), Solana keeps one monolithic, very fast layer-1 and pays for the speed with beefier validators. It is not a free lunch — heavier hardware means fewer people can afford to validate, which is real centralization pressure — but it is a coherent, deliberate point on the trade-off surface.
The build order, and the fairness baseline
Section titled “The build order, and the fairness baseline”solmini won’t reproduce a validator — no signatures, no staking, no networking. It models the two ideas that make Solana fast, and the part builds them in dependency order:
the-throughput-problem ← you are here: the motivation proof-of-history the verifiable SHA-256 clock accounts-and- stateless programs + typed account buffers stateless-programs (the declared footprint becomes possible) account-locks- the conflict scheduler: batches of non-conflicting txs scheduler assembling- parallel execution across a thread pool, the-runtime then commit-on-success and rollback-on-failureThere is one more idea to plant now, because it governs everything after: the fairness baseline. Before you are allowed to run transactions in parallel, you first write the boring, obviously-correct version — run_sequential, which applies transactions strictly in submission order, one at a time. That baseline is the definition of the right answer.
/// Run transactions strictly in submission order, one at a time. This is the/// simple, obviously-correct baseline the parallel path must match.pub fn run_sequential(&mut self, txs: &[Transaction]) -> Vec<Result<()>> { let mut results = Vec::with_capacity(txs.len()); for tx in txs { let mut working = self.working_set(tx); let r = Self::run_tx(&self.programs, self.compute_per_tx, tx, &mut working); if r.is_ok() { self.commit(tx, &working); // commit only on success } results.push(r); } results}The parallel scheduler you build later must produce exactly the same final state as this sequential run — not “close,” not “usually,” but bit-for-bit identical, verified by integration tests that run both paths and compare the account database. That is the contract: parallelism is an optimization, never a change in meaning. If the fast path and the baseline ever disagree, the fast path is wrong. Holding a slow, correct reference next to a fast, subtle one is how you build a system that runs at hardware speed without falling apart — which is the second half of the book’s question, and the reason the sequential baseline exists at all.
The architect’s lens
Section titled “The architect’s lens”The technology in view is declare-your-footprint parallel execution (Solana’s Sealevel model) — the decision to make transactions announce which accounts they touch so the runtime can parallelize them.
- Why does it exist? Because a state machine that discovers each transaction’s footprint only by executing it is forced to execute serially, capping the whole chain at single-core speed. Declaring footprints up front is the minimum information a runtime needs to schedule work across cores.
- What problem does it solve? It converts idle CPU cores into throughput: the runtime can prove which transactions are independent before running them and dispatch those in parallel, safely.
- What are the trade-offs? Programs must be stateless with state in named external accounts, and every transaction must list its accounts — more friction for developers, and a hard cap when many transactions want the same hot account (they still serialize).
- When should I avoid it? When your workload is inherently sequential or dominated by a few contended accounts, the declaration overhead buys little; and if simple, low-throughput correctness is all you need, the naive serial VM is easier to reason about.
- What breaks if I remove it? Without declared footprints the runtime is back to running one transaction at a time, and the parallel scheduler, the multi-core throughput, and the “hardware speed” half of the whole design vanish.
Check your understanding
Section titled “Check your understanding”- Explain, from first principles, why a naive world-computer with contracts that hold their own storage is forced to execute transactions one at a time.
- What single piece of information does the sequential design lack that makes parallelism impossible — and what change supplies it?
- State the conflict rule between two transactions. Which pairs are safe to run on different cores, and which must be serialized?
- How does the declare-your-footprint rule map onto Rust’s shared-XOR-mutable borrow rule? Give the correspondence for read-only and writable accounts.
- Why does the runtime build a
run_sequentialbaseline before the parallel scheduler, and what exact property must the parallel path satisfy relative to it?
Show answers
- Because the state a call touches is decided as the code runs (branches, computed keys, cross-contract calls), the runtime cannot know a transaction’s footprint until it has executed it. Without footprints it can’t tell whether two transactions conflict, so running them concurrently risks a data race and a network fork — the only safe choice is to run them strictly in order, which pins throughput to a single core.
- It lacks each transaction’s footprint (the set of accounts it reads and writes) before execution. Requiring every transaction to declare its read and write sets up front supplies it, turning conflict detection into a cheap set comparison the runtime can do without running anything.
- Two transactions conflict when they share at least one account and at least one of them writes it. Transactions with disjoint footprints — or that only read the same account (read–read is not a conflict) — are safe to run on different cores; any pair where one writes an account the other also uses must be serialized.
- A read-only account is like an
&Tshared reference — many transactions may hold it at once; a writable account is like an&mut Texclusive reference — exactly one transaction may hold it at a time. A conflict-free batch is exactly a set of borrows the borrow checker would accept, which is why lifting shared-XOR-mutable from variables to accounts makes concurrent execution safe. - The sequential run is the simple, obviously-correct definition of the right answer. The parallel path is an optimization, so it must produce a bit-for-bit identical final state to the sequential run (verified by tests that run both and compare the account database); if they ever disagree, the parallel path is the one that’s wrong.