Every Solana instruction declares which accounts it touches and how. Those declarations are not documentation — the runtime uses them to decide what can execute in parallel and what has to wait.
★Getting them wrong rarely produces an error. It produces a transaction that works and quietly competes with more transactions than it needed to.★
The Two Flags
const keys = [
{ pubkey: userWallet, isSigner: true, isWritable: true }, // pays and changes
{ pubkey: poolAccount, isSigner: false, isWritable: true }, // ★changes★
{ pubkey: tokenMint, isSigner: false, isWritable: false }, // read only
{ pubkey: TOKEN_PROGRAM_ID, isSigner: false, isWritable: false },
];
isSigner — this account's signature must be present. Getting it wrong fails loudly: Missing required signature.
isWritable — this instruction may modify the account. ★Getting this wrong is where the silent cost lives.★
Why isWritable Decides Your Throughput
Solana executes transactions in parallel. The scheduler works out what can run together by looking at the writable account sets:
tx A writes [pool_1] ┐ ★parallel — no overlap★
tx B writes [pool_2] ┘
tx C writes [pool_1] ┐ ★serialised — same writable account★
tx D writes [pool_1] ┘
★Marking an account writable when you only read it puts your transaction in a queue it did not need to join.★ Two reads of the same account run in parallel; two writes do not.
For a bot sending many transactions against popular accounts, this is not theoretical. Over-declaring writable accounts is one of the few mistakes that costs throughput without producing a single error message.
// ★Read the mint, do not write it.★
{ pubkey: tokenMint, isSigner: false, isWritable: false },
The check worth running: for every account you marked writable, ask whether the program actually modifies it. Mints, program IDs, sysvars, and config accounts are almost always read-only.
If your instructions are correct and transactions still land late, a free BoltTx key is one line to test the submission path.
Order Is Part of the Interface
Programs read accounts positionally. accounts[0], accounts[1], and so on — the names in an IDL are for humans, and the runtime passes an array.
// ★Swap two of these and the program reads the wrong account.★
const keys = [
{ pubkey: source, isSigner: false, isWritable: true },
{ pubkey: destination, isSigner: false, isWritable: true },
{ pubkey: authority, isSigner: true, isWritable: false },
];
Reversing source and destination here does not fail validation. Both are writable token accounts of the right type. ★It transfers in the wrong direction★, and the transaction reports success.
This is the failure mode worth fearing — not a revert, but a successful transaction that did something other than what you intended. Anchor's IDL protects you when you use its client; hand-built instructions have no such guardrail.
Deduplication and the Fee Payer
Solana deduplicates account keys across a transaction, and merges the flags:
// Same account in two instructions.
ix1: { pubkey: X, isWritable: false }
ix2: { pubkey: X, isWritable: true }
// ★Merged: X is writable for the whole transaction.★
★The union wins.★ One instruction declaring an account writable makes it writable for scheduling purposes across the entire transaction — so a single over-declaration in one instruction affects contention for all of them.
Two related rules that catch people:
The fee payer is always index 0, always signer, always writable. It pays a fee, so its balance changes by definition.
Signers come before non-signers, and within each group writable comes before read-only. The SDK handles this ordering when you build a TransactionMessage, which is one reason hand-assembling the account array is riskier than it looks.
Reading What You Actually Sent
Before debugging why a transaction behaved oddly, look at what you built:
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
message.staticAccountKeys.forEach((key, i) => {
console.log(
i,
key.toBase58(),
message.isAccountSigner(i) ? "signer" : "",
message.isAccountWritable(i) ? "writable" : "read-only",
);
});
★isAccountWritable reports the merged result, which is what the scheduler sees.★ It is common for this output to differ from what any single instruction declared, and that difference is exactly what you want to inspect.
What Goes Wrong, and How It Presents
| Mistake | Symptom |
|---|---|
Missing isSigner |
★Missing required signature — loud★ |
Extra isSigner |
Signature verification fails |
Missing isWritable |
★readonly data modified — loud★ |
Extra isWritable |
★Nothing. Throughput quietly drops.★ |
| Wrong order | ★Success, wrong behaviour★ |
| Missing account | NotEnoughAccountKeys |
★The two rows without loud failures are the dangerous ones.★ Everything else tells you immediately.
Account Metas and Transaction Size
Each account key costs 32 bytes toward the 1232-byte limit, so the account list is usually what makes a complex transaction too large.
console.log("accounts:", message.staticAccountKeys.length);
console.log("account bytes:", message.staticAccountKeys.length * 32);
Two reductions worth knowing:
Duplicates are free. Referencing the same account in five instructions costs 32 bytes once, because of the deduplication above.
Unused accounts are not. An account you pass but the program never touches still costs its 32 bytes. ★Copying an account list from an example and leaving entries you do not need is a common way to approach the size ceiling for no benefit.★
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★Account metas affect the contention you face; routing affects the path you take to face it.★ A transaction with a minimal writable set still has to reach a block producer, and that half is unchanged by how carefully you declared your accounts.
Where BoltTx Fits
We handle submission. Instruction construction is entirely yours — we never modify transaction contents, which includes never touching your account metas.
Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands.
You sign locally. We never hold funds and never sign. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.
Get a free API key. No monthly fee:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
What is an AccountMeta in Solana?
A declaration of one account an instruction touches, carrying its public key plus isSigner and isWritable flags. The runtime uses those flags for signature checks and for parallel scheduling.
What does isWritable do? It declares that the instruction may modify the account. It also determines scheduling: transactions writing the same account are serialised, while transactions that only read it can run in parallel.
What happens if I mark an account writable unnecessarily? No error. Your transaction joins the contention queue for that account without needing to, which reduces throughput silently. This is the most common account meta mistake.
Why does my transaction say readonly data modified?
The program tried to modify an account you declared read-only. Set isWritable: true for that account, or check whether you are passing the wrong account in that position.
Does account order matter in a Solana instruction? Yes. Programs read accounts positionally, so a wrong order can produce a transaction that succeeds while doing the wrong thing — for example transferring in the reverse direction.
How does Solana deduplicate accounts in a transaction? Repeated keys appear once, and the flags are merged as a union. If any instruction declares an account writable, it is writable for the whole transaction as far as scheduling is concerned.
Is the fee payer always writable? Yes, and always a signer, and always at index 0. Paying the fee changes its balance, so it is writable by definition.
How do I check which accounts are writable in my transaction?
Compile the message and call isAccountWritable for each index. It reports the merged result the scheduler sees, which often differs from any single instruction's declaration.
What is the account ordering rule?
Signers before non-signers, and within each group writable before read-only. The SDK arranges this when you build a TransactionMessage, which is a reason to prefer it over hand-assembling.
Does passing extra accounts cost anything? Yes, 32 bytes each toward the 1232-byte transaction limit, even if the program never uses them. Copied-in account lists with unused entries approach the size ceiling for no benefit.
Why do I get NotEnoughAccountKeys? The program expected more accounts than you passed. Every account a program reads must be listed explicitly, including ones only touched inside a CPI.
Should a program ID be marked writable? No. Program accounts are read-only during execution, and marking them writable adds contention against every transaction that uses that program.
Do sysvars need to be writable? No. Sysvars such as the rent and clock accounts are read-only, and many programs no longer require them to be passed at all.
How do account metas affect parallel execution? The scheduler compares writable sets. Transactions with no writable account in common can execute together, so a smaller writable set means less serialisation.
Does Anchor handle account metas for me? Yes, when you use the generated client, since the IDL carries the order and flags. Hand-built instructions have no such guardrail, which is why order mistakes show up there.
How do I reduce the number of accounts in a transaction? Remove accounts the program does not use, reduce routing hops, and use address lookup tables to compress references. Duplicates are already free, so consolidating references does not help.
Related Reading
- Solana Versioned Transactions
- Solana PDA Derivation
- Solana Transaction Simulation
- Solana Transaction Error Codes
- Solana Transaction Landing