Parallel EVM Execution: How to Process Transactions Concurrently

Explore parallel EVM execution with dependency analysis, wave-based scheduling, speculative execution, and conflict detection for 5-6x throughput gains.

By Zig EVM Team |

TL;DR: Sequential transaction processing is the primary throughput bottleneck of EVM-based blockchains. Parallel execution can achieve 5-6x or higher throughput by running independent transactions concurrently. This article explains the dependency analysis, wave-based scheduling, and speculative execution techniques implemented in the Zig EVM, with benchmarks and code examples.

The Sequential Bottleneck

Ethereum processes transactions one at a time, in order. Transaction 47 must complete before transaction 48 begins. This design is simple and guarantees deterministic state transitions, but it wastes enormous computational resources.

Consider a block with 200 transactions. If 150 of them touch completely different contracts and storage slots, there is no reason they cannot execute simultaneously. Yet the sequential model forces each one to wait for every predecessor, regardless of whether any actual data dependency exists.

The Ethereum Virtual Machine processes approximately 15-30 transactions per second on mainnet, not because of computational limits, but because of sequential execution ordering. Modern hardware can execute thousands of independent EVM transactions per second if parallelism is exploited.

Why Parallel Execution Is Hard

The difficulty is not parallelism itself — it is determining which transactions can safely run in parallel.

State Conflicts

Two transactions conflict if they access the same state and at least one of them writes:

Transaction ATransaction BConflict?Reason
Reads slot XReads slot XNoRead-read is safe
Reads slot XWrites slot XYesRead-write dependency
Writes slot XReads slot XYesWrite-read dependency
Writes slot XWrites slot XYesWrite-write conflict
Sends ETH to addr ASends ETH to addr BNoDifferent addresses
Sends ETH to addr ASends ETH to addr AYesBalance conflict

Types of Dependencies

The Zig EVM’s parallel execution engine tracks three categories of dependencies:

  1. Storage conflicts: Two transactions read/write the same storage slot in the same contract
  2. Balance conflicts: Two transactions modify the same account’s balance (transfers to/from the same address)
  3. Nonce conflicts: Two transactions from the same sender must maintain nonce ordering
// Dependency detection: check if two transactions conflict
fn hasConflict(tx_a: *const Transaction, tx_b: *const Transaction) bool {
    // Nonce conflict: same sender
    if (std.mem.eql(u8, &tx_a.from, &tx_b.from)) return true;

    // Balance conflict: overlapping addresses
    if (std.mem.eql(u8, &tx_a.to, &tx_b.to)) return true;
    if (std.mem.eql(u8, &tx_a.from, &tx_b.to)) return true;
    if (std.mem.eql(u8, &tx_a.to, &tx_b.from)) return true;

    // Storage conflict: overlapping read/write sets
    for (tx_a.write_set) |slot_a| {
        for (tx_b.read_set ++ tx_b.write_set) |slot_b| {
            if (slot_a == slot_b) return true;
        }
    }

    return false;
}

Dependency Analysis Approaches

Before executing transactions in parallel, you must build a dependency graph. There are two fundamental approaches.

O(n squared) Pairwise Comparison

The basic approach compares every transaction pair:

// Basic dependency analysis: O(n^2)
// Compare every pair of transactions for conflicts
fn buildDependencyGraph(transactions: []Transaction) DependencyGraph {
    var graph = DependencyGraph.init();
    for (transactions, 0..) |tx_a, i| {
        for (transactions[i + 1 ..], i + 1..) |tx_b, j| {
            if (hasConflict(&tx_a, &tx_b)) {
                graph.addEdge(i, j);
            }
        }
    }
    return graph;
}

This is straightforward but scales poorly. For a block with 1,000 transactions, this requires approximately 500,000 comparisons. For 10,000 transactions, 50 million.

O(n) Hash-Based Analysis

The Zig EVM’s optimized parallel engine uses hash-based conflict detection that runs in O(n) time:

// Optimized dependency analysis: O(n) hash-based
// Each address/slot maps to the last transaction that accessed it
fn buildDependencyGraphOptimized(transactions: []Transaction) DependencyGraph {
    var graph = DependencyGraph.init();
    var last_writer = std.HashMap(u256, usize).init(); // slot -> tx index
    var last_reader = std.HashMap(u256, usize).init();

    for (transactions, 0..) |tx, i| {
        // Check write set against previous readers and writers
        for (tx.write_set) |slot| {
            if (last_writer.get(slot)) |prev| graph.addEdge(prev, i);
            if (last_reader.get(slot)) |prev| graph.addEdge(prev, i);
            last_writer.put(slot, i);
        }
        // Check read set against previous writers
        for (tx.read_set) |slot| {
            if (last_writer.get(slot)) |prev| graph.addEdge(prev, i);
            last_reader.put(slot, i);
        }
    }
    return graph;
}

This approach provides a 10-100x speedup in dependency analysis for large transaction batches compared to pairwise comparison. The tradeoff is that it may identify slightly more dependencies than strictly necessary (it is conservative), but the execution time savings far outweigh the minor reduction in parallelism.

ApproachTime Complexity1,000 txs10,000 txs
PairwiseO(n^2)~500K comparisons~50M comparisons
Hash-basedO(n)~3K lookups~30K lookups
Speedup~160x~1,600x

Wave-Based Parallel Execution

Once the dependency graph is built, the Zig EVM groups transactions into “waves” — sets of transactions that have no dependencies on each other and can execute simultaneously.

Wave Construction Algorithm

  1. Identify all transactions with no unresolved dependencies (in-degree = 0 in the dependency graph)
  2. Group them into a wave
  3. Execute the entire wave in parallel
  4. Mark completed transactions, update dependency counts
  5. Repeat until all transactions are executed
// Wave-based execution: group independent transactions
fn createWaves(graph: *DependencyGraph, num_transactions: usize) [][]usize {
    var waves = std.ArrayList([]usize).init();
    var in_degree = computeInDegrees(graph, num_transactions);
    var completed = std.AutoHashMap(usize, void).init();

    while (completed.count() < num_transactions) {
        var wave = std.ArrayList(usize).init();

        // Collect all transactions with no remaining dependencies
        for (0..num_transactions) |i| {
            if (!completed.contains(i) and in_degree[i] == 0) {
                wave.append(i);
            }
        }

        // Execute this wave in parallel
        waves.append(wave.toOwnedSlice());

        // Update dependencies
        for (wave.items) |completed_tx| {
            completed.put(completed_tx, {});
            for (graph.successors(completed_tx)) |dependent| {
                in_degree[dependent] -= 1;
            }
        }
    }

    return waves.toOwnedSlice();
}

Visualizing Waves

Consider 8 transactions with these dependencies:

TX0 -> TX3       (TX3 depends on TX0)
TX1 -> TX4       (TX4 depends on TX1)
TX2 -> TX5       (TX5 depends on TX2)
TX3 -> TX6       (TX6 depends on TX3)
TX7 (independent)

The wave decomposition:

Wave 1: [TX0, TX1, TX2, TX7]  -- 4 transactions in parallel
Wave 2: [TX3, TX4, TX5]       -- 3 transactions in parallel
Wave 3: [TX6]                 -- 1 transaction

Instead of 8 sequential steps, execution completes in 3 parallel waves. With sufficient threads, this represents a 2.67x speedup on this workload.

Speculative Execution with Rollback

The optimized Zig EVM parallel engine goes beyond static dependency analysis with speculative execution.

How Speculative Execution Works

  1. Execute optimistically: Run all transactions in parallel without waiting for dependency analysis
  2. Track access sets: Record every storage slot and address each transaction reads and writes during execution
  3. Validate: After execution, check if any concurrent transactions have conflicting access sets
  4. Rollback and retry: If conflicts are detected, roll back the conflicting transaction and re-execute it
// Speculative execution: execute first, validate later
fn executeSpeculative(
    transactions: []Transaction,
    thread_pool: *ThreadPool,
) []ExecutionResult {
    var results = allocator.alloc(ExecutionResult, transactions.len);

    // Phase 1: Execute all transactions in parallel
    for (transactions, 0..) |tx, i| {
        thread_pool.spawn(executeWithTracking, .{ &tx, &results[i] });
    }
    thread_pool.waitAll();

    // Phase 2: Validate -- detect conflicts from recorded access sets
    var conflicts = detectConflicts(results);

    // Phase 3: Re-execute conflicting transactions sequentially
    for (conflicts) |tx_index| {
        results[tx_index] = executeSequential(transactions[tx_index]);
    }

    return results;
}

When Speculative Execution Wins

Speculative execution is most effective when conflicts are rare. For typical Ethereum blocks:

Workload TypeConflict RateSpeculative Benefit
Token transfers (diverse recipients)Low (under 5%)High — most txs are independent
DEX swaps (different pools)Low-Medium (5-15%)Good — distinct liquidity pools
NFT minting (same contract)Medium (15-30%)Moderate — counter/index conflicts
Airdrop (same sender)High (over 90%)Low — nonce serialization required

Thread Pool Implementation

The Zig EVM implements two thread pool designs for parallel execution.

Basic Thread Pool

The basic parallel engine uses a simple thread pool where each wave is distributed across available threads:

// Basic thread pool: one thread per transaction in the wave
fn executeWave(wave: []usize, transactions: []Transaction) void {
    var threads: [MAX_THREADS]std.Thread = undefined;
    const thread_count = @min(wave.len, MAX_THREADS);

    for (0..thread_count) |t| {
        threads[t] = std.Thread.spawn(.{}, executeSingle, .{
            transactions[wave[t]],
        });
    }

    for (0..thread_count) |t| {
        threads[t].join();
    }
}

Work-Stealing Thread Pool

The optimized engine uses a work-stealing thread pool for better load balancing. When a thread finishes its transaction early, it steals work from other threads’ queues:

// Work-stealing: idle threads take tasks from busy threads
fn workStealingLoop(self: *Worker, all_workers: []Worker) void {
    while (true) {
        // Try own queue first
        if (self.queue.pop()) |task| {
            task.execute();
            continue;
        }

        // Own queue empty: steal from a random other worker
        const victim = randomWorker(all_workers, self.id);
        if (victim.queue.steal()) |task| {
            task.execute();
            continue;
        }

        // Nothing to steal: check if all work is done
        if (allQueuesEmpty(all_workers)) break;
    }
}

Work stealing is especially valuable when transactions have variable execution times. A simple token transfer might take 50 microseconds while a complex DeFi interaction takes 2 milliseconds. Without work stealing, the thread handling the complex transaction becomes a bottleneck.

Benchmarks and Results

We benchmarked the Zig EVM’s parallel execution across different workloads and thread counts.

Throughput Scaling

ThreadsSequential (tx/s)Basic Parallel (tx/s)Optimized Parallel (tx/s)
112,40012,100 (overhead)11,800 (more overhead)
222,80023,400
441,20045,600
862,40071,200
1668,10074,800

Key observations:

  • Linear scaling up to 8 threads with optimized parallel execution
  • Diminishing returns beyond 8 threads due to dependency serialization and memory bandwidth limits
  • The optimized engine’s overhead (hash-based analysis, access tracking) is offset by better parallelism on larger batches

Dependency Analysis Overhead

Batch SizePairwise Analysis (ms)Hash-Based Analysis (ms)Speedup
1000.80.0516x
1,000780.4195x
10,0007,8003.82,053x

For blocks with thousands of transactions, the O(n) hash-based analysis is not just faster — it is the difference between practical and impractical.

Running the Benchmarks

# Run the parallel execution demo
zig build parallel

# Run the optimized parallel execution demo
zig build parallel-opt

# Run performance benchmarks
zig build benchmark

# Run with release optimizations for accurate timing
zig build benchmark -Doptimize=ReleaseFast

You can also experiment with parallel execution concepts in the Zig EVM Playground.

State Merging: Maintaining Consistency

After parallel execution, results from multiple threads must be merged into a consistent global state. This is non-trivial because each thread operates on its own copy of relevant state.

Merge Strategy

The Zig EVM uses a copy-on-write strategy:

  1. Each thread receives a snapshot of the pre-block state
  2. During execution, writes go to a thread-local delta map
  3. After wave completion, deltas are merged into the global state in transaction order
// State merging: apply deltas in transaction order
fn mergeWaveResults(
    global_state: *State,
    results: []ExecutionResult,
    wave: []usize,
) void {
    // Sort by original transaction index to maintain ordering
    std.sort.sort(usize, wave, {}, lessThan);

    for (wave) |tx_index| {
        const delta = results[tx_index].state_delta;
        for (delta.storage_writes) |write| {
            global_state.setStorage(write.address, write.slot, write.value);
        }
        for (delta.balance_changes) |change| {
            global_state.updateBalance(change.address, change.delta);
        }
    }
}

The transaction-order merge ensures that the final state is identical to what sequential execution would have produced. This is essential for consensus compatibility.

Comparison with Other Parallel EVM Approaches

ProjectApproachDependency DetectionRollback Strategy
Zig EVMWave-based + speculativeHash-based O(n)Per-transaction rollback
MonadOptimistic parallelRuntime conflict detectionFull re-execution
Sei v2DAG-based schedulingPre-declared access listsOrdered re-execution
PolygonParallel proof generationBlock-level independenceNot applicable
Aptos (Move VM)Resource-basedStatic analysis of resourcesAbort and retry

The Zig EVM’s approach balances implementation complexity with performance. Wave-based scheduling is simpler than full DAG-based execution but captures most of the available parallelism.

Key Takeaways

  • Sequential execution is a throughput bottleneck, not a computational one. Modern hardware can execute thousands of EVM transactions per second, but sequential ordering limits mainnet to approximately 15-30 transactions per second.
  • Dependency analysis determines the ceiling for parallelism. The O(n) hash-based approach in the Zig EVM is 100-2000x faster than pairwise comparison for realistic batch sizes.
  • Wave-based scheduling captures most available parallelism with relatively simple implementation. Transactions are grouped into waves where every transaction in a wave is independent.
  • Speculative execution maximizes throughput when conflicts are rare, which is the common case for diverse Ethereum workloads. Rollback costs are acceptable at conflict rates below 15-20%.
  • Scaling is near-linear up to 8 threads for the Zig EVM, achieving 5-6x throughput improvement over sequential execution.
  • State merging must preserve transaction ordering to maintain consensus compatibility with sequential execution.

The Parallel EVM Landscape in 2026

Several projects are tackling parallel EVM execution with different approaches:

ProjectLanguageApproachThroughput ClaimStatus
Zig EVMZigWave-based scheduling + speculative execution5-6x over sequentialOpen source, embeddable
MonadC++Optimistic parallel execution + async I/O10,000 TPSL1, mainnet launched
Sei v2GoOptimistic parallelization with dependency graphs12,500 TPSL1, live
MegaETHRustReal-time parallel processing + state sharding100,000+ TPSL2, testnet
Reth (Ethereum)RustExperimental parallel state accessN/A (research)Execution client
Neon EVMRust/SolanaSolana-native parallel execution for EVM txsSolana throughputLive on Solana

Key differences in approach:

  • Monad uses optimistic execution where all transactions run in parallel and conflicts are detected post-execution. This works well for their custom L1 where they control block building and can reorder transactions to minimize conflicts.
  • Sei v2 builds explicit dependency graphs before execution, similar to the Zig EVM’s wave-based approach but integrated into a full L1 consensus layer.
  • Zig EVM focuses on embeddability — it provides parallel execution as a library that can be integrated into any application, rather than being tied to a specific blockchain. The wave-based approach with O(n) hash-based dependency analysis makes it practical for offline batch processing, testing frameworks, and custom L2 sequencers.

The fundamental insight shared by all these projects is the same: most transactions in a typical block are independent, and identifying that independence is the key to unlocking parallelism.

Getting Started with Parallel Execution

Explore the Zig EVM’s parallel execution implementation:

# Clone the repository
git clone https://github.com/cryptuon/zig-evm.git
cd zig-evm

# Run the basic parallel demo
zig build parallel

# Run the optimized parallel demo with work stealing
zig build parallel-opt

# Run benchmarks with release optimizations
zig build benchmark -Doptimize=ReleaseFast

The full source code is available at github.com/cryptuon/zig-evm. The parallel execution modules are in src/parallel.zig (basic) and src/parallel_optimized.zig (optimized with speculative execution).

Try the Zig EVM Playground to visualize transaction execution and understand the state changes that drive dependency analysis.

Frequently Asked Questions

What is parallel EVM execution?
Parallel EVM execution processes multiple blockchain transactions simultaneously instead of one at a time. Transactions that do not access the same state (storage slots, balances) can safely execute in parallel, yielding throughput improvements of 5-6x or more.
Why can't all EVM transactions run in parallel?
Transactions that read or write the same storage slots, modify the same account balances, or depend on each other's nonce values have data dependencies. Executing them in parallel would produce inconsistent state. Dependency analysis identifies which transactions conflict and must run sequentially.
What is speculative execution in the context of EVM?
Speculative execution runs transactions in parallel optimistically, assuming no conflicts. If a conflict is detected after execution (e.g., two transactions wrote to the same storage slot), the conflicting transaction is rolled back and re-executed. This approach maximizes parallelism when conflicts are rare.
How much faster is parallel EVM execution?
The Zig EVM achieves 5-6x throughput improvement for independent transaction batches using parallel execution. The speedup depends on the workload: batches with many conflicting transactions see less benefit, while DeFi workloads with distinct contract interactions scale nearly linearly.
Which blockchain projects use parallel EVM execution?
Several projects implement parallel EVM execution, including Monad (optimistic parallel execution), Sei v2 (parallel EVM with dependency graphs), MegaETH (real-time parallel processing), and the Zig EVM (wave-based parallel execution with speculative rollback). Ethereum's base layer processes transactions sequentially.

Try Zig EVM

Explore the interactive playground or dive into the source code.