A fast instruction can still become a throughput bottleneck when every user
writes the same account. Solana can schedule independent state transitions in
parallel, but a shared writable account turns otherwise separate transactions
into competing work.
This guide explains the conflict boundary and gives you five architecture
patterns plus a reusable Solana Contention Review. The goal is not to remove
every shared account. It is to make serialization an intentional business rule
instead of an accidental data-model choice.
For the underlying account model, start with
Solana accounts explained.
Read and write declarations define the conflict surface
Every Solana transaction message declares the accounts its instructions use.
Accounts that may change data or lamports must be writable. The runtime uses
those declarations to protect state and schedule compatible work.
Use this matrix as the first diagnostic:
| Transaction A | Transaction B | Can their account access overlap safely? |
| --- | --- | --- |
| reads account X | reads account X | yes, both are read-only |
| writes account X | reads account X | conflict around X |
| writes account X | writes account X | conflict around X |
| writes account X | writes account Y | no lock conflict if X and Y are distinct |
This is not a promise about exact execution order or landing. It describes the
account conflict surface the scheduler must respect.
A CPI cannot upgrade an account to writable when the transaction did not grant
that privilege. Mark only required accounts writable, and pass the correct
privileges through every CPI boundary.
Pattern 1: move user state out of the global account
A single global account is convenient for configuration. It is a poor place for
unrelated per-user balances, positions, counters, or claim records.
Prefer one program-derived account per independent entity when the business
rules allow it:
#[derive(Accounts)]
#[instruction(market: Pubkey)]
pub struct UpdatePosition<'info> {
#[account(
mut,
seeds = [b"position", market.as_ref(), authority.key().as_ref()],
bump = position.bump,
has_one = authority,
)]
pub position: Account<'info, Position>,
pub authority: Signer<'info>,
}Two users can now mutate distinct position accounts. Keep global configuration
read-only during the update unless the instruction truly changes it.
Partitioning changes the security model. Bind every shard to its authority,
market, mint, or parent state. The
program security guide covers the
owner, signer, seeds, and relationship checks this requires.
Pattern 2: separate configuration from live counters
Configuration often changes rarely, while volume, sequence, rewards, or usage
counters change on every action. Putting them in one account forces every action
to request the configuration account as writable.
Split them only when the invariant remains clear:
- immutable or rarely changed configuration account
- partitioned operational state for active writes
- explicit administrative instruction for configuration changes
Readers can share the configuration account. Writers contend only on the state
they update.
Do not copy authoritative configuration into every shard without a versioning
rule. Otherwise concurrency improves while consistency quietly decays.
Pattern 3: shard aggregation without losing the invariant
Leaderboards, rewards, supply totals, and rate limits often appear to require one
global counter. Sometimes they do. Sometimes the product only needs an exact
total at settlement time.
A safe staged design can use:
- deterministic shards for frequent writes
- a separate aggregation instruction
- an epoch or sequence that prevents double counting
- a canonical finalized total for decisions that require exactness
Choose the shard key from a stable domain such as market, epoch, region, or a
hash bucket. Never let the client choose an arbitrary shard if that choice can
bypass a cap or concentrate rewards.
The tradeoff is delayed aggregation and more complex reads. If every instruction
must observe an exact global total before proceeding, serialization may be the
correct design.
Pattern 4: keep read-only accounts read-only through CPIs
Clients and frameworks can over-declare mutability. Review the final compiled
message, not only the source-level account struct.
For each writable account, ask:
- which instruction changes its data or lamports?
- which program owns the mutation?
- does a CPI actually require writable privilege?
- can the result be returned or emitted without changing shared state?
- is the account writable only because a helper API marked it that way?
Solana's scheduler cost model includes write-lock cost, and current documentation
lists a 12M-CU per-writable-account block limit. Treat that value as a
current runtime boundary, not a permanent design constant.
Reducing unnecessary writable accounts improves the declared conflict surface.
It does not compensate for expensive program logic. Use the
compute-unit optimization guide
for that separate loop.
Pattern 5: make contention visible to clients and operators
Contention symptoms are easy to mislabel as random RPC failure. Preserve a small
trace for each operation:
type ContentionTrace = {
operationId: string;
instructionFamily: string;
writableAccounts: string[];
simulationSlot?: number;
unitsConsumed?: number;
signature?: string;
lastValidBlockHeight?: number;
error?: unknown;
attempts: number;
};Group failures and latency by instruction family and writable account. A single
address appearing across the slow tail is stronger evidence than a generic
“network busy” label.
Retries must respect transaction lifetime and product idempotency. Do not create
a newly signed transaction while the original can still land unless the
operation is safe to repeat. Follow the
transaction reliability guide for
blockheight-aware confirmation.
When evidence points to RPC lag, forwarding, or status visibility rather than
state topology, RPC Edge is the relevant boundary. RPC
infrastructure can expose and route around delivery problems. It cannot remove a
shared writable account from your program design.
The Solana Contention Review
Run this review before mainnet and after each new high-volume instruction:
- list every account in the final message and its read/write privilege
- identify writable addresses shared across unrelated users or markets
- state the invariant that requires each shared write
- move independent state to authority-bound or domain-bound PDAs
- separate rarely changed configuration from high-frequency operational state
- assess sharding only where delayed aggregation preserves correctness
- inspect CPI account metas for unnecessary writable privileges
- load-test common and worst valid conflicts with realistic state
- record writable accounts, slots, attempts, errors, and confirmation outcomes
- document which operations intentionally serialize and why
The final artifact should name each hot account, its invariant, expected writers,
partition key if any, and the client behavior under contention.
FAQ, sources, and next step
Are read-only accounts free of all limits?
No. They still affect message construction, loading, and scheduler cost. The key
concurrency difference is that multiple transactions can read the same account
without competing for a write lock.
Will a higher priority fee solve account contention?
It may improve a transaction's scheduling position. It does not let conflicting
transactions mutate the same account in parallel or repair an overloaded state
topology.
Should global protocol state always be sharded?
No. Exact shared invariants can require serialization. Shard only when the
invariant, aggregation model, and authorization remain correct.
How do I find the hot account?
Capture the final writable account list per instruction and group latency,
errors, and attempts by address. Confirm the pattern with load tests and
transaction traces before changing the data model.
Primary sources
- Solana core concepts
- Solana compute-budget scheduler costs and writable-account limit
- Solana program architecture and parallel state
- Solana CPI writable privilege rules
Use the transaction debugging guide
to trace a failing instruction before changing its state topology. If a hot
account crosses program design, client retries, and RPC behavior,
try a Builderz contention review.



