Optimizing Contracts for Parallelization
Sei EVM executes non-conflicting transactions in parallel. You can significantly increase throughput and reduce gas by designing contracts that minimize shared state access and avoid unnecessary storage writes.
This guide is based on Sei’s recommendations for reducing gas usage and enhancing parallel execution. For the engine design, see Parallelization Engine.
Principles for Parallel-Friendly Contracts
- Minimize shared writes: The scheduler can parallelize transactions that do not write to the same storage keys. Avoid hot globals (e.g., a single counter updated on every call).
- Partition state: Shard storage by user, asset, or id so independent transactions touch disjoint keys.
- Prefer pull over push: Let users claim funds instead of mass-paying recipients in loops.
- Avoid unbounded loops: Especially loops that write to storage or iterate over dynamic arrays/mappings.
- Batch internal work; isolate external effects: Do heavy computation in memory and commit a minimal set of storage writes.
- Use precompiles when available: Precompiles are highly optimized and cheaper than replicating logic in Solidity.
Parallelization checklist:
- Partition storage by user/asset/id; avoid hot globals
- Minimize number of storage writes per call; batch in-memory, commit once
- Avoid loops with storage writes; switch to pull-based flows
- Favor precompiles for supported features
- Apply standard gas optimizations (
external
, packing,unchecked
, memory-first)
Storage Design Patterns
Partition state by key
Isolate per-user/per-asset data instead of centralizing writes.
// Good: disjoint keys by user and id enable parallelism
mapping(address => mapping(uint256 => Position)) public positions;
function updatePosition(uint256 id, int256 delta) external {
Position storage p = positions[msg.sender][id];
// in-memory arithmetic first
int256 newQty = p.qty + delta;
// commit minimal writes
p.qty = newQty;
}
Anti-patterns:
- Writing a global
totalVolume += amount;
in every transaction - Maintaining a single on-chain queue or registry updated by most calls
Prefer computing aggregates off-chain from events, or update them periodically via a dedicated maintenance transaction and place them in the calldata.
Prefer pull payments
Avoid writing to many recipients in a single transaction. Emit events and let recipients withdraw()
when needed.
// Better: users pull their own rewards, isolating writes to their key
mapping(address => uint256) public accrued;
function accrue(address user, uint256 amount) internal {
accrued[user] += amount; // isolated write
}
function withdraw() external {
uint256 due = accrued[msg.sender];
accrued[msg.sender] = 0; // single-key write
(bool ok, ) = msg.sender.call{value: due}("");
require(ok, "TRANSFER_FAILED");
}
Avoid large storage writes in loops
If you must process many items, keep work in memory and commit a compact result, or split work across multiple transactions keyed by different ids.
Gas-Efficient Solidity Practices
- Use
external
for externally called functions; mark pure/read-only withpure
/view
. - Pack variables to share storage slots (e.g., multiple
uint8
in one slot). - Prefer
bytes32
overstring
when applicable. - Cache array lengths in loops and short-circuit cheap conditions first.
- Use
unchecked
for arithmetic when overflow is impossible. - Prefer memory over storage for temporary data; commit only final values.
These reduce gas and often reduce storage touches, which also improves parallelism.
Leverage Sei Precompiles
Sei provides precompiled contracts for common functionality—cheaper and simpler than custom Solidity equivalents:
See precompile examples for quick starts.
Testing and Analysis
- Use Foundry/Hardhat gas reporters to track changes per function.
- Benchmark under concurrent load to detect hot keys (addresses/ids) that serialize execution.
- Inspect execution with JavaScript tracers and runtime logs.