EVM vs SVM: Why Solana Runs in Parallel

1. Overview

Ethereum Virtual Machine (EVM) processes transactions sequentially, each block is a queue of operations one after another. Solana Virtual Machine (SVM) is the opposite: it runs tens of thousands of transactions per second in parallel across multiple CPU cores. This article explains why there’s such a difference — not because SVM is ‘faster’ by magic, but because its account model tells the runtime upfront who will touch whom. After reading, you’ll understand the Sealevel mechanism, be able to simulate the parallel scheduler in Java yourself, and know when this model fails.

2. EVM’s Sequential Execution Model

EVM stores the entire state in a shared Merkle Patricia Trie tree for all smart contracts. A transaction calling the increment() function of contract A doesn’t declare upfront which storage slot it will touch — that’s only known at runtime, because EVM is a stateful, Turing-complete machine where branches (if/else) can decide which slot to write to depending on input data.

contract Counter {
    uint256 public count;

    function increment() public {
        count += 1; // single storage slot, not declared to anyone upfront
    }
}

count is a fixed storage slot, but the EVM runtime has no way to know upfront — before executing the bytecode — that this function will read/write exactly that slot without trying it first. Because ‘knowing upfront’ is impossible with arbitrary bytecode, Ethereum clients (geth, Nethermind…) are forced to execute transactions sequentially within a block, ensuring that each transaction always sees the state updated by previous transactions.

2.1. Why Conflicts Cannot Be Predicted Upfront

There’s no sufficiently cheap ‘static analysis’ mechanism to analyze every path of EVM bytecode and infer the set of affected storage slots before execution. Some projects like Monad or Block-STM (Aptos) solve this with optimistic concurrency: run things in parallel, and if conflicts are detected, rollback and retry. That’s a ‘execute first, check later’ solution — quite different from what SVM does, which is ‘declare upfront, schedule later’.

EVM Block (default mainnet):
tx1 -> tx2 -> tx3 -> tx4   (sequential, one at a time)

3. SVM’s Explicit Account Model (Sealevel)

Solana requires every transaction to explicitly declare the list of accounts it will read or write, right in the transaction message, before sending to the network. The runtime doesn’t need to try running it to know — the client has already spelled it out.

pub fn process_instruction(
    _program_id: &Pubkey,
    accounts: &[AccountInfo],
    _instruction_data: &[u8],
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
    let counter_account = next_account_info(accounts_iter)?;

    let mut data = counter_account.try_borrow_mut_data()?;
    let mut count = u64::from_le_bytes(data[0..8].try_into().unwrap());
    count += 1;
    data[0..8].copy_from_slice(&count.to_le_bytes());
    Ok(())
}

accounts is a slice passed in from outside — the client must clearly specify which account counter_account is, whether it’s writable or read-only, when building the transaction. The program cannot ‘discover’ additional accounts on its own along the way. It’s this constraint — seemingly restrictive — that enables Sealevel to schedule transactions in parallel.

3.1. Declaring Writable/Readonly Right in the Transaction

Transaction message (simplified):
  instruction: increment_counter
  accounts:
    - counter_pda      (writable)
    - fee_payer        (writable, signer)
    - system_program   (readonly)

The runtime only needs to read this metadata — without executing any bytecode — to know that this transaction will write to counter_pda and fee_payer, and read system_program. This is the fundamental difference compared to EVM.

4. Conflict Detection and Parallel Batch Scheduling Mechanism

Once the runtime has the read-set/write-set of each transaction, the parallel scheduling problem becomes a familiar graph coloring problem: two transactions can run concurrently if the sets of accounts they access are disjoint in terms of write-write or write-read operations.

public record BlockTransaction(String id, Set<String> writes, Set<String> reads) {}

writes and reads correspond to accounts that the transaction will modify and only read, respectively. Two transactions conflict when:

public final class SealevelScheduler {

    public static boolean conflicts(BlockTransaction a, BlockTransaction b) {
        return !Collections.disjoint(a.writes(), b.writes())
            || !Collections.disjoint(a.writes(), b.reads())
            || !Collections.disjoint(a.reads(), b.writes());
    }
}

Explanation of each condition:

  • writes ∩ writes non-empty → both want to write the same account → must be sequential.
  • writes ∩ reads non-empty → one reads, one writes the same account → must be sequential.
  • reads ∩ reads non-empty → that’s fine, concurrent reads don’t need exclusive locks.

4.1. Java Simulation: Group into Batches and Run in Parallel

public final class ParallelExecutor {

    private final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor();

    public List<BlockTransaction> nextNonConflictingBatch(
            Deque<BlockTransaction> queue, Set<String> lockedAccounts) {

        List<BlockTransaction> batch = new ArrayList<>();
        for (BlockTransaction tx : queue) {
            boolean overlaps = Stream.concat(tx.writes().stream(), tx.reads().stream())
                    .anyMatch(lockedAccounts::contains);
            if (!overlaps) {
                batch.add(tx);
                lockedAccounts.addAll(tx.writes());
            }
        }
        queue.removeAll(batch);
        return batch;
    }

    public void runBatch(List<BlockTransaction> batch, Consumer<BlockTransaction> execute) {
        List<CompletableFuture<Void>> futures = batch.stream()
                .map(tx -> CompletableFuture.runAsync(() -> execute.accept(tx), pool))
                .toList();
        futures.forEach(CompletableFuture::join);
    }
}

nextNonConflictingBatch scans the transaction queue, accepts any transaction that doesn’t touch the current lockedAccounts into a batch, then locks the accounts it writes to. runBatch uses virtual threads (Java 21) to run the entire batch concurrently, join waits for the whole batch to finish before forming the next batch — exactly how Sealevel scheduler groups transactions by ‘wave’.

Note: this is a simple simulation to illustrate the idea. Real Sealevel uses more complex algorithms (dependency graphs per account, not just sequential batches) to maximize parallelism.

4.2. Verifying Conflict Logic with JUnit 5

class SealevelSchedulerTest {

    @Test
    void differentAccountsNoConflict() {
        var tx1 = new BlockTransaction("tx1", Set.of("walletA"), Set.of());
        var tx2 = new BlockTransaction("tx2", Set.of("walletB"), Set.of());

        assertThat(SealevelScheduler.conflicts(tx1, tx2)).isFalse();
    }

    @Test
    void sameWriteAccountConflict() {
        var tx1 = new BlockTransaction("tx1", Set.of("poolAccount"), Set.of());
        var tx2 = new BlockTransaction("tx2", Set.of("poolAccount"), Set.of());

        assertThat(SealevelScheduler.conflicts(tx1, tx2)).isTrue();
    }

    @Test
    void concurrentReadsNoConflict() {
        var tx1 = new BlockTransaction("tx1", Set.of(), Set.of("priceOracle"));
        var tx2 = new BlockTransaction("tx2", Set.of(), Set.of("priceOracle"));

        assertThat(SealevelScheduler.conflicts(tx1, tx2)).isFalse();
    }
}

These three tests precisely describe the principle: read-read requires no lock, write-write requires a lock, write-read requires a lock — the foundation of the entire SVM parallelization problem.

5. EVM vs SVM Comparison

Criterion EVM SVM
State Model Single shared trie, implicit access through bytecode Discrete accounts, explicit declaration in transaction
When Conflicts Are Known Only known after execution (or trial execution) Known before execution, from transaction metadata
Execution Sequential in block order Parallel in non-overlapping batches
Cross-contract Call Call other contracts arbitrarily at runtime Cross-Program Invocation (CPI) still must declare accounts upfront
Developer Responsibility No need to think about account sets Must declare accounts correctly and completely — missing causes errors, excess hurts parallelism
Parallelism Improvement Direction Optimistic concurrency (Block-STM, Monad) Optimize dependency graph scheduling (Sealevel, Firedancer)

6. Trade-offs and Common Errors When Writing Solana Programs

Explicit account declaration is the source of parallelism, but it’s also the source of two common errors:

Missing Account Declaration. If the program’s logic needs to write to an account that the client didn’t list in the transaction, the program fails immediately at runtime with an error like ‘account not found in accounts list’ — because SVM doesn’t allow ‘discovering’ accounts mid-execution like EVM does.

Declaring Too Many or Creating Hotspots. If too many transactions write to the same account (like a single liquidity pool, a global counter), they’re forced to serialize — exactly like EVM, except you created it yourself. This is a common thinking error for developers used to EVM: bringing the habit of ‘one global variable’ to Solana without sharding accounts.

Wrong: all user transactions write to `global_stats` -> loses parallelism
Right: each user has their own account (PDA per user), aggregate periodically

7. When Parallelism Cannot Help

Note: if your application is inherently a true sequential queue (like an auction pricing bids by submission order), runtime parallelism doesn’t help — you still need to serialize business logic yourself; SVM only saves on the independent transactions.

Similarly, if you’re designing a centralized system (not blockchain) but want to ‘learn from’ the Sealevel model to increase throughput, it’s only worthwhile when most tasks have clearly distinct read/write sets. If every task touches a common table/record, the cost of managing the dependency graph is higher than simple sequential locking.

8. Conclusion

EVM runs sequentially because its bytecode doesn’t tell the runtime upfront who will touch whom — any conflict prediction can only be done by trial execution or rollback. SVM runs in parallel because it forces every transaction to declare read/write accounts upfront, allowing Sealevel to build a dependency graph and form non-overlapping batches to run on multiple cores. The trade-off is that Solana developers must design accounts to be separate, avoiding hotspots — otherwise, you inadvertently turn SVM back into a slow, sequential EVM.

svmevmsolanablockchainparallel-computing