How Solana's Sealevel Runtime Executes Transactions in Parallel
Wrote a 1,300-word explainer on Solana's Sealevel parallel execution — account-level locking mechanics, a two-part Rust worked example (global vs. sharded counter), and design implications, with required internal links woven in and verified against the AI-tell/word-count/link constraints.
{"excerpt":"Solana Sealevel parallel execution explained: how account-level locking lets the runtime run transactions concurrently, and how it shapes program design.","tags":["Solana","Sealevel","Parallel Execution","Account Model","Smart Contract Architecture"],"cover":"nodes","content":"Ethereum executes one transaction at a time, full stop — every EVM client, no matter how many cores it has, replays the chain serially because a transaction can touch any storage slot in any contract and there's no way to know which ones until the bytecode actually runs. Solana's runtime, Sealevel, takes the opposite bet: force every transaction to declare its account list up front, and use that list to run non-conflicting transactions across many CPU cores at once. That one design decision is why Solana processes thousands of transactions per second on commodity hardware, and why the account structure you pick for a program has direct, measurable consequences for throughput and fee competitiveness.\n\n## The account list is the whole trick\n\nA Solana transaction isn't just "call this program with this data." Every transaction carries a Message containing a flat array of account public keys, each tagged with two bits of metadata: is_signer and is_writable. The instruction itself only references accounts by index into that array. This means that before the runtime executes a single instruction, it already knows exactly which accounts the transaction will read and which it will mutate.\n\nCompare that to the EVM, where a CALL opcode can jump into an unknown contract that touches unknown storage. Solana programs can't do this — every account a program will touch, including accounts reached via cross-program invocation, has to be present in that top-level list. There's no dynamic account discovery mid-transaction. That's a real constraint (it's why Address Lookup Tables exist — a transaction caps out around 1232 bytes on the wire, and a long account list eats that budget fast), but it's also the entire mechanism that makes parallelism possible.\n\n## How the runtime builds the lock table\n\nWhen a validator's leader schedule turns to your node, incoming transactions land in the banking stage. Before a transaction gets handed to a worker thread for execution, the scheduler walks its account list and tries to acquire locks:\n\n- Write lock on any account marked is_writable — exclusive, only one transaction can hold it\n- Read lock on any account only read — shared, many transactions can hold read locks on the same account simultaneously\n\nTwo transactions can execute concurrently if and only if their write sets don't intersect and their write sets don't intersect the other's read set. Read-read never conflicts. Write-write always conflicts. Write-read conflicts in either direction. If your transaction wants to write to an account another in-flight transaction is reading, you wait.\n\nThis is deterministic and cheap to check — it's set intersection over pubkeys, not simulation. That's the whole reason it scales: the cost of deciding "can these two run together" is O(accounts), not O(execution).\n\nAgave's scheduler batches non-conflicting transactions into separate execution threads — historically around 4 to 8 parallel execution units per leader slot, tuned by validator hardware. Firedancer's scheduler does the same job with a different implementation and materially different throughput under load; our breakdown of Firedancer versus Agave covers the scheduling differences in more detail.\n\n## A worked example: the counter that doesn't scale\n\nTake the simplest possible Anchor program — a global counter:\n\nrust\n#[account]\npub struct Counter {\n pub count: u64,\n}\n\npub fn increment(ctx: Context<Increment>) -> Result<()> {\n let counter = &mut ctx.accounts.counter;\n counter.count = counter.count.checked_add(1).unwrap();\n Ok(())\n}\n\n#[derive(Accounts)]\npub struct Increment<'info> {\n #[account(mut)]\n pub counter: Account<'info, Counter>,\n}\n\n\nEvery call to increment declares counter as writable. If a thousand users call this instruction in the same slot, every single one of those transactions writes to the same pubkey. The lock table serializes all thousand of them — it doesn't matter that the validator has 32 cores available, this program gets exactly one core's worth of throughput because there is exactly one writable account and only one transaction can hold its write lock at a time.\n\nNow shard it. Give each user their own counter PDA, seeded by their own pubkey:\n\nrust\n#[derive(Accounts)]\npub struct Increment<'info> {\n #[account(\n mut,\n seeds = [b\"counter\", user.key().as_ref()],\n bump\n )]\n pub counter: Account<'info, Counter>,\n pub user: Signer<'info>,\n}\n\n\nSame logic, same instruction, but now a thousand simultaneous increments touch a thousand distinct accounts. None of them conflict. The scheduler fans them out across every available execution thread. This is the entire lesson condensed into eight lines of Rust: parallelism in Sealevel isn't something the runtime gives you for free, it's something your account layout either permits or forbids.\n\n## Where this bites real programs\n\nNobody ships a bare counter, but the same pattern shows up constantly in production code, usually disguised as something that sounds reasonable:\n\n- A single global "protocol stats" account tracking total volume or TVL, written on every trade\n- One PDA holding a fee-collection balance that every swap increments\n- A shared sequence number or nonce account used to order events\n- A single liquidity pool account on a low-cap pair during a launch spike, where every buyer's transaction writes to the same reserve balances\n\nThat last one is worth sitting with, because it's not a design mistake — it's inherent to how AMMs work. A constant-product pool's reserves live in one account by necessity; every swap against that pool writes to it. During a token launch, every sniper racing to buy off the same bonding curve is contending for that same write lock, and no clever program design removes the contention, because a shared price curve is the whole point. What changes is who wins the lock: priority fees and Jito tip bids become the real arbitration mechanism once the scheduler treats every contender as equally entitled to it. We've covered how Jito bundles and priority fees interact as MEV protection separately, and that mechanic is directly downstream of the locking behavior described here — it's why our sniper bot builds put as much engineering into fee bidding as into detection latency.\n\n## Designing around it\n\nA few patterns hold up across the programs we've audited and built:\n\n- Separate hot writes from cold reads. Put fields that change every transaction (balances, counters, timestamps) in their own account, apart from config data that's read constantly but written rarely. A read-heavy account never blocks parallelism; a write-heavy one bundled with unrelated config does.\n- Shard anything with per-user semantics. Balances, positions, and order state belong in per-user PDAs, not a shared table. It's the difference between a market-making engine that scales across pairs and one that self-contends against its own orders.\n- Accept serialization where it's genuinely required. A global oracle feed or a protocol-wide pause flag has to be a single account — plan for it as a bottleneck instead of fighting it, since compute budget doesn't buy you around a lock conflict.\n- Watch writes, don't poll for them. A wallet tracker reacting to a whale's next move or an arbitrage bot waiting on a pool's reserves needs account updates the moment they land. Yellowstone gRPC streams them, which polling can't match.\n- Mind the account list limit. Anything touching many accounts per instruction — a router hitting five pools, a bundler firing multiple buys in one shot — needs Address Lookup Tables to fit the transaction size cap, and every account in that list still participates in locking. A longer list is more contention surface, not less.\n\nSealevel doesn't parallelize your program for you. It parallelizes the parts of your program that don't share writable state, and it leaves you to decide how much of your state that actually is. Every account you mark mut is a decision about how much of your protocol's throughput you're willing to serialize.\n\nIf you're designing or auditing a program where lock contention determines whether your transactions land ahead of the field, that's exactly the kind of architecture review we do for MEV and arbitrage infrastructure on Solana.\n"}
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article