← All weekly notes

17 min read

Executing blockchain transactions in parallel

A distributed-systems view of transaction conflicts, deterministic execution, and how Ethereum, Solana, Aptos, Sui and Monad expose or discover parallelism.

Distributed systems · Blockchain · Ethereum · Solana · Parallel execution

I was wondering how Ethereum transactions could be executed concurrently.

A block may contain hundreds of transactions, and many of them appear unrelated. One user may swap tokens, another may mint an NFT, and a third may update a completely different contract. If these transactions do not affect one another, why should every CPU core wait while a single execution thread processes them one by one?

That question led me to Solana's transaction model, then to Aptos Block-STM, Sui's object model and Monad's parallel EVM execution. The interesting part is not simply that some chains are "parallel." Each system answers the same distributed-systems question differently:

How can every validator use several CPU cores while still producing exactly the same final state from the same ordered block?

The hard part is not starting several workers. The hard part is discovering dependencies, preserving transaction semantics, and making the result deterministic on every machine.

The first correction: the conflict is not just an address

My initial model was that two transactions conflict when they touch the same address. That is a useful starting point, but it is too coarse for Ethereum.

The real unit is a state key. A state key might represent:

  • An account nonce.
  • An account balance.
  • A contract's code or existence.
  • A specific contract storage slot.
  • In an object-based chain, a particular object and version.

Two transactions conflict only when their access sets overlap and at least one access is a write.

Transaction ATransaction BConflict?
Read xRead xNo
Read xWrite xYes
Write xRead xYes
Write xWrite xYes
Write xWrite yNo

This distinction immediately reveals two details that an address-level model misses.

First, two calls to the same Ethereum contract may be independent if they access different storage slots. Updating balances[Alice] and balances[Bob] in a mapping can produce different storage keys, although both calls target the same token contract.

Second, calls to different top-level addresses may still conflict. Contract A can call contract C, while contract B also calls C. Both execution paths may update the same pool, oracle, registry or token balance. The destination address in the transaction envelope does not reveal the full dependency set.

I use this notation for a transaction Ti:

R(Ti) = state keys read by TiW(Ti) = state keys written by Ti

Two transactions conflict when:

W(Ti) intersects R(Tj), orR(Ti) intersects W(Tj), orW(Ti) intersects W(Tj)

Read/read overlap is safe because neither transaction changes what the other observes.

Ordering and execution are different problems

A blockchain must first agree on which transactions belong in a block and in what order. That is the consensus or sequencing problem.

After ordering, every validator executes the block to calculate the new state, receipts and other execution results. That is the execution problem.

unordered transactions        │        ▼ consensus chooses an order        │        ▼  T0 → T1 → T2 → T3        │        ▼ execute that ordered block        │        ▼ identical final state on every validator

Parallel execution does not mean validators can choose arbitrary outcomes. It means they may calculate independent work concurrently, provided the committed result is equivalent to the chain's canonical ordering rules.

Ethereum's formal state-transition model is sequential. If σ is the starting state and Υ applies one transaction, a block is evaluated conceptually as:

σ1 = Υ(σ0, T0)σ2 = Υ(σ1, T1)σ3 = Υ(σ2, T2)

An implementation may speculate in parallel, but the observable result must still match T0, then T1, then T2. This property is usually called serial equivalence or serializability.

That requirement is what turns a CPU scheduling question into a concurrency-control problem.

A small block with real dependencies

Consider five transactions in an already-agreed block order:

T0  Alice transfers to BobT1  Carol swaps in pool PT2  Bob transfers to EveT3  Dave mints from contract MT4  Frank swaps in pool P

A simplified access trace could be:

T0: W(Alice.nonce, Alice.balance, Bob.balance)T1: W(Carol.nonce, Carol.balance, PoolP.reserves)T2: W(Bob.nonce, Bob.balance, Eve.balance)T3: W(Dave.nonce, Dave.balance, MintM.supply, MintM.owner[42])T4: W(Frank.nonce, Frank.balance, PoolP.reserves)

T0, T1 and T3 can begin together. T2 must observe Bob's balance after T0, and T4 must observe pool P after T1.

The useful parallel schedule is therefore:

wave 1: T0   T1   T3wave 2: T2   T4

This schedule is not derived from arrival time or transaction destination alone. It comes from the actual state dependencies.

Building the dependency graph

If complete access sets were known before execution, constructing the graph would be straightforward. For each pair in block order, add an edge from the earlier transaction to the later transaction when their state accesses conflict.

Here is the core test in Go:

package scheduler type StateKey string type AccessSet struct {	Reads  map[StateKey]struct{}	Writes map[StateKey]struct{}} func overlaps(left, right map[StateKey]struct{}) bool {	for key := range left {		if _, ok := right[key]; ok {			return true		}	}	return false} func conflicts(a, b AccessSet) bool {	return overlaps(a.Writes, b.Reads) ||		overlaps(a.Reads, b.Writes) ||		overlaps(a.Writes, b.Writes)}

A scheduler can run every zero-dependency transaction, wait for it to finish, remove its outgoing edges, and release newly ready transactions. This is a parallel topological traversal of the dependency graph.

The problem for the EVM is that a complete access set usually is not known before execution.

Why Ethereum dependencies are difficult to predict

An Ethereum transaction declares its sender, recipient, value, calldata, gas parameters and signature. It does not provide a binding list of every account and storage slot it will read or write.

The path can depend on current state:

if (pool.price() < limit) {    router.swap(...);       // touches pool, tokens and recipient balances} else {    vault.deposit(...);     // touches a different set of contracts and slots}

Nested contract calls make this more dynamic. CALL, DELEGATECALL, contract creation, callbacks and token hooks can reveal new dependencies only while the EVM is running. A proxy may select an implementation from storage. A router may choose a pool based on a quote. The same calldata can touch different keys against different starting states.

Ethereum access lists do not fully solve this. EIP-2930 lets a transaction predeclare addresses and storage keys so they start warm and receive different gas treatment. Accesses outside the list are still allowed. The list is therefore a useful prefetching hint, not a complete concurrency contract on which a scheduler can safely rely.

Other sources of contention are easy to overlook:

  • Transactions from one sender update the same nonce and normally form a strict sequence.
  • Transfers can update a shared recipient balance.
  • Many swaps converge on the same pool reserve slots.
  • An NFT mint can update one global supply counter.
  • Protocols can route through shared token, oracle or registry contracts.
  • Block-level values and gas accounting must retain their specified semantics.

The EVM's flexibility is valuable, but it hides the state footprint until runtime.

One approach for Ethereum: optimistic execution

When dependencies cannot be trusted in advance, an execution engine can discover them by running the transactions.

The basic optimistic algorithm is:

  1. Consensus provides a fixed block order.
  2. Workers execute several transactions speculatively against a versioned state view.
  3. Each execution records the keys and versions it read and the writes it wants to publish.
  4. Results are validated and committed in canonical transaction order.
  5. If an earlier transaction changed a value that a later transaction read, the later result is stale and is executed again.

Suppose T0 and T1 both read version 7 of PoolP.reserves. T0 is earlier in the block and commits version 8. When T1 reaches validation, its recorded read of version 7 is no longer valid. Its speculative output is discarded and it runs again using version 8.

worker A:  execute T0 on PoolP@7 ───────────────┐                                                ├─ commit T0 → PoolP@8worker B:  execute T1 on PoolP@7 ─ stale ───────┘                       │                       └─ re-execute T1 on PoolP@8 → commit

A stripped-down result type looks like this:

type Version uint64 type Read struct {	Key     StateKey	Version Version} type Result struct {	Index  int	Reads  []Read	Writes map[StateKey][]byte} func valid(result Result, committedVersions map[StateKey]Version) bool {	for _, read := range result.Reads {		if committedVersions[read.Key] != read.Version {			return false		}	}	return true}

Real engines need multi-version storage, dependency tracking, abort handling, gas and receipt correctness, memory limits, scheduling and a safe serial fallback. The small example only captures the central invariant: a transaction may commit only if every value it relied on is still the value it should have observed in the preset order.

This is optimistic concurrency control applied inside a replicated state machine.

Solana: declare the accounts, then lock them

Solana was the first model I examined because it moves dependency information into the transaction itself.

Solana programs are stateless code. Mutable state lives in accounts passed to instructions. A transaction message lists its accounts and marks them read-only or writable. The runtime can therefore see the transaction's coarse access set before program execution.

The scheduling rule is approximately:

read A + read A     → may run togetherread A + write A    → must not run togetherwrite A + write A   → must not run togetherdisjoint accounts   → may run together

The runtime locks writable accounts while the transaction is being processed. Transactions with compatible account sets can occupy different execution lanes. Instructions inside one transaction still execute sequentially and atomically: every instruction succeeds, or the transaction's state changes are reverted.

This is closer to pessimistic concurrency control. Solana avoids much speculative re-execution because it obtains exclusive access before executing a writer.

Why the declaration is enforceable

A program cannot silently mutate any account it discovers. The required accounts are part of the instruction context, and writable privilege is explicit. Cross-program invocations carry those privileges forward; a callee cannot escalate a read-only account into a writable one.

That restriction makes the state footprint visible enough for scheduling.

The tradeoff: account-level contention

Solana's scheduler understands accounts, not arbitrary independent fields inside one account. If a program stores a global counter, a large order book or all user balances in one writable account, every transaction updating any part of it contends on the same lock.

Application design therefore affects runtime parallelism. Splitting independent state across program-derived accounts can expose more parallel work. A badly chosen shared writable account creates a hotspot even when the logical operations are unrelated.

The advantage is predictability. The cost is that developers must model state around explicit accounts and reason about hot writable accounts.

Aptos: Block-STM discovers conflicts dynamically

Aptos takes a different route. Transactions have a preset order, but Block-STM executes them optimistically using software transactional memory.

The main pieces are:

  • A block-wide order chosen before execution.
  • A multi-version data structure holding speculative writes.
  • A read set that records which versions each transaction observed.
  • Execution and validation tasks managed by a collaborative scheduler.
  • Re-execution when validation finds that an earlier transaction changed a dependency.

The preset order is useful rather than merely restrictive. If transaction T7 needs a value produced by T3, the engine knows exactly which earlier transaction owns the dependency. It can suspend or invalidate work and retry with the correct version.

Block-STM guarantees a deterministic outcome consistent with serial execution in that preset order. Validators can make different timing decisions locally—one machine may finish T8 before T4—without producing different committed states.

What happens under contention

Optimism works well when conflicts are sparse. If 32 transactions update unrelated accounts, 32 workers can make progress. If all 32 update the same counter, most early speculative work becomes invalid.

low conflict:   execute in parallel → mostly validate → commithigh conflict:  execute in parallel → abort/retry → approach serial work

An adaptive scheduler should avoid repeatedly launching work that it already knows is blocked on an earlier transaction. Even then, a hot key limits the available parallelism. The engine can reduce overhead; it cannot manufacture independence that the workload does not contain.

Sui: make dependencies objects

Sui changes the state model more fundamentally. State is organized as objects with identity, ownership and version. Transactions name their input objects.

The important distinction is between owned and shared objects.

Owned objects

An owned object has one owner. A transaction consuming a particular object version receives an exclusive lock or certificate for that version. Two unrelated users transferring their own objects do not need a global order between those operations because neither can affect the other's inputs.

This exposes a causal partial order:

AliceCoin@5 → Alice transfer → AliceCoin@6 BobNFT@2    → Bob transfer   → BobNFT@3

The two chains of causality can progress independently.

Shared objects

A shared object can be used by multiple parties. Transactions involving shared objects require sequencing so every validator agrees which version each transaction receives. Once input versions are assigned, transactions without causal dependencies can still execute concurrently.

A popular shared DEX pool remains a hotspot. Sui does not make simultaneous writes to one object magically commutative. Instead, its object model isolates owned-state operations from shared-state coordination and makes object dependencies explicit.

This resembles partitioned ownership in a distributed system: keep independent state under independent ownership, and pay for agreement only when writers truly share state.

Monad: preserve EVM semantics with ordered optimistic commit

Monad is useful as a comparison because it keeps EVM bytecode compatibility and Ethereum's linearly ordered block semantics while executing transactions in parallel.

Its documented model is optimistic. Later transactions may start before earlier transactions finish. The engine tracks the inputs they used, then merges state updates sequentially in block order. If an earlier output changes an input consumed by a later transaction, the later transaction is executed again.

This demonstrates an important point:

Parallel execution does not require changing the EVM application's visible transaction order.

The implementation can exploit parallel CPU work internally while preserving the same result an Ethereum-style serial execution would produce. Work that is independent succeeds on its first attempt. Conflicting work pays for validation and re-execution.

That makes Monad conceptually close to the Block-STM family, although implementation details and scheduling policies differ.

Four concurrency-control families

The blockchain designs map cleanly to familiar database and distributed-systems techniques.

SystemHow dependencies become visibleConcurrency approachMain contention unit
Ethereum semanticsDiscovered during EVM executionCanonical serial order; clients may use serial-equivalent optimizationsAccount fields and storage slots
SolanaTransaction declares read-only and writable accountsPessimistic account locking and parallel lanesAccount
AptosRuntime records reads and speculative writesBlock-STM optimistic execution, validation and retryVersioned state location
SuiTransaction names versioned input objectsObject ownership; sequence shared objectsObject
MonadRuntime tracks inputs and outputs during EVM executionOptimistic parallel execution with ordered merge and retryEVM state location

I now think of them as four broad families:

Serial state machine      → avoid concurrency conflicts by not overlapping workDeclared access + locks   → reveal dependencies before executionOptimistic execution      → discover dependencies while executing, then validateObject ownership          → encode independence into the state model

No family dominates every workload.

Determinism is the non-negotiable property

In an ordinary database, two concurrent schedules may both be valid if they satisfy the isolation contract. In blockchain execution, validators must also agree on one exact outcome.

Nondeterminism can enter through:

  • Different worker completion orders.
  • A race selecting which write wins.
  • Iteration over unordered data structures.
  • Machine clocks or local randomness.
  • Inconsistent error, event or receipt ordering.
  • Re-execution that charges different gas or exposes different block context.

A correct engine separates physical execution order from logical commit order. Workers may finish in any order, but state effects become canonical according to protocol rules.

physical completion: T3, T1, T0, T2logical commit:      T0, T1, T2, T3

The state root, transaction success or failure, logs, gas accounting and receipts must match the specified logical history.

Why a hot contract still becomes serial

Parallelism is a property of the workload, not only the virtual machine.

Imagine a block of 1,000 transfers between unrelated accounts. There is abundant independent work. Now imagine 1,000 swaps against one AMM pool whose reserves live in two shared slots. Each swap depends on the reserves produced by the previous swap.

The dependency chain is almost entirely serial:

swap 0 → reserves 1 → swap 1 → reserves 2 → swap 2 → ...

More cores cannot shorten the critical path. An optimistic engine may even do extra work by repeatedly executing swaps against stale reserves.

This is Amdahl's law expressed as state contention. If a fraction of block execution is inherently serial, it limits total speedup no matter how efficiently the parallel portion runs.

Application architecture matters as much as runtime architecture:

  • Per-user vaults expose more parallelism than one global balance map when the chain schedules at account or object granularity.
  • Sharded order books reduce contention but make cross-shard matching harder.
  • Batched updates can reduce coordination frequency but increase latency.
  • Commutative operations such as some counters may support specialized merging, but general smart-contract writes are not commutative.

Parallel execution is not the same as sharding

These terms are often mixed together.

Parallel execution uses several cores or workers to process one ordered block or state domain faster. Every validating node may still store and execute the same global state.

Sharding divides data or responsibility across groups of nodes. A node may process only a subset of state, and cross-shard operations need routing, proofs and coordination.

parallel execution:one validator → many cores → one global state result sharding:shard A nodes ↔ cross-shard protocol ↔ shard B nodes

The techniques can be combined, but they solve different bottlenecks. Parallel execution reduces local computation time. Sharding tries to distribute storage, bandwidth and execution across the network.

Engineering costs that throughput numbers can hide

An execution benchmark is only one view of the design. A production engine also has to handle:

Abort storms

An attacker or naturally hot application can create many conflicts. Optimistic workers may consume CPU and memory for results that never commit. Schedulers need backoff, dependency awareness and a serial fallback.

Versioned-state memory

Speculative execution retains read sets, write sets and multiple versions until results are validated. Wide blocks or long-running transactions can increase memory pressure.

Scheduling overhead

For very small transactions, graph management, queues, synchronization and validation can cost more than serial execution. Parallelism has a break-even point.

State storage bandwidth

More execution workers can overwhelm the state database. A fast VM does not help if every worker stalls on random storage reads. Prefetching, caching and database layout become part of the execution design.

Fairness and hotspots

A transaction touching a popular account or object can wait behind a long queue. The protocol must decide how fees, priority and resource limits interact with contested state.

Debuggability

The canonical result is deterministic, but the internal schedule may not be. Reproducing a performance problem requires traces of dependencies, aborts, retries and commit delay, not only transaction logs.

A practical way to reason about any chain

When reading another blockchain's execution design, I now ask these questions in order.

1. What is the state unit?

Is it an account, an EVM storage slot, a Move resource, a versioned object or a database key?

2. How are reads and writes discovered?

Are they declared before execution, inferred from the program, or recorded optimistically at runtime?

3. What orders conflicting transactions?

Does consensus provide one total order, does the runtime build a dependency order, or are only shared objects sequenced?

4. What happens after a conflict?

Does one transaction wait for a lock, abort and retry, execute on a newer version, or fail at submission?

5. What result must be preserved?

Must execution match a preset serial order, or does the protocol define a partial order over independent state?

6. What is the hotspot behavior?

Does the system fall back gracefully toward serial execution, reject competing locks, or waste speculative work?

7. Where is the bottleneck after adding cores?

It may move from the VM to state storage, memory, consensus, network propagation or one popular application object.

These questions are more useful than asking only whether a chain supports parallel execution.

What I learned

My original question—"Can Ethereum execute transactions concurrently?"—now has a more precise answer.

Yes, independent transaction work can be executed concurrently by an implementation, but Ethereum's observable result must preserve its canonical ordered state transition. The difficulty is that EVM transactions do not provide complete, binding read and write sets before execution. An engine must predict, discover or speculatively record those dependencies.

Solana makes dependencies visible through declared writable accounts and locks them before execution. Aptos discovers fine-grained dependencies with Block-STM and retries stale work. Sui makes ownership and versions part of the object model, reserving stronger sequencing for shared objects. Monad shows how optimistic parallelism can sit behind EVM-compatible semantics.

The deeper distributed-systems lesson is this:

Parallel transaction execution is concurrency control inside a deterministic replicated database.

The number of CPU cores is the easy part. The design is really about state granularity, dependency discovery, ownership, validation and a canonical commit order.

Further reading