
Solana docs: how do I use v0 transactions and Address Lookup Tables to fit more accounts into one transaction?
Quick Answer: Use Solana’s v0 versioned transactions together with Address Lookup Tables (ALTs) to reference more accounts with fewer bytes. v0 messages let you “compress” 32-byte account addresses into 1-byte indices stored in ALTs, allowing you to push beyond the practical 32-account ceiling of legacy transactions while staying within packet and compute limits.
Why This Matters
If you’re building serious payment flows, DeFi routes, or multi-party settlement on Solana, you hit transaction limits fast: too many accounts, too much state, not enough room in a single packet. Versioned (v0) transactions and Address Lookup Tables turn that constraint into a tool—letting you pack more accounts into a single atomic operation without blowing up packet size or fee budgets. That means fewer round trips, fewer reconciliation headaches, and workflows that feel like real-time payments, not batch files.
Key Benefits:
- More accounts per transaction: Reference many more than 32 accounts by loading them from ALTs via 1-byte indices instead of full 32-byte keys.
- Smaller messages, lower overhead: Save 31 bytes for every ALT-resolved account, helping you stay under Solana’s packet limits without sacrificing complexity.
- Cleaner, more atomic workflows: Fit full payment, routing, or DeFi operations into one transaction instead of coordinating partial updates and retries.
Core Concepts & Key Points
| Concept | Definition | Why it's important |
|---|---|---|
| Legacy vs v0 transactions | Legacy transactions encode all account keys inline; v0 transactions use the versioned format that supports ALTs. Validators infer the format from the message’s first byte. | Legacy format effectively caps usable accounts around 32 per transaction; v0 is required to leverage ALTs and push beyond that ceiling. |
| Address Lookup Tables (ALTs) | On-chain tables that store up to 256 related addresses, referenced by 1-byte indices instead of 32-byte public keys. | ALTs “compress” addresses, allowing more accounts in a transaction while keeping message size within Solana’s packet limits. |
| ALT-resolved accounts in v0 | Accounts that are not listed directly in the v0 message but are loaded at runtime by validators using the ALT indices. | They extend the usable account list per transaction (e.g., raising the practical limit from 32 to 64 and beyond) and are key to complex, multi-account workflows. |
How It Works (Step-by-Step)
At a high level, you:
- Create an Address Lookup Table.
- Extend it with the account addresses you need.
- Build a v0 transaction that references those accounts by their ALT indices instead of full keys.
Under the hood, validators use the v0 message’s lookup table sections to resolve indices to full 32-byte addresses at runtime, then execute your instructions against the full account list.
1. Create an Address Lookup Table
You can create an ALT with either a legacy transaction or a v0 transaction, but:
NOTE: Only v0 versioned transactions can actually use ALT addresses at runtime.
In TypeScript with @solana/web3.js:
import {
Connection,
PublicKey,
TransactionInstruction,
Transaction,
sendAndConfirmTransaction,
} from '@solana/web3.js';
import {
AddressLookupTableProgram,
} from '@solana/web3.js';
const connection = new Connection('https://api.mainnet-beta.solana.com');
async function createLookupTable(payer: PublicKey, recentSlot: number) {
const [instruction, lookupTableAddress] =
AddressLookupTableProgram.createLookupTable({
authority: payer,
payer,
recentSlot,
});
const tx = new Transaction().add(instruction);
await sendAndConfirmTransaction(connection, tx, /* signers */);
return lookupTableAddress;
}
Key facts:
- Each ALT can store up to 256 addresses.
- You typically group related addresses: e.g., all token accounts for a specific route, all PDAs in a protocol, or a portfolio of counterparties.
- The authority can later extend or close the table.
2. Extend the Lookup Table with Addresses
Adding addresses to a lookup table is called extending the table.
async function extendLookupTable(
payer: PublicKey,
lookupTableAddress: PublicKey,
newAddresses: PublicKey[],
) {
const extendIx = AddressLookupTableProgram.extendLookupTable({
payer,
authority: payer,
lookupTable: lookupTableAddress,
addresses: newAddresses,
});
const tx = new Transaction().add(extendIx);
await sendAndConfirmTransaction(connection, tx, /* signers */);
}
Operational notes:
- Plan the address set. Each extension costs a transaction and some fees; treat ALT layout like schema design.
- You can extend multiple times until you hit the 256-address cap.
- ALTs are shared: multiple transactions and programs can use the same table as long as their account lists are compatible.
3. Build a v0 Transaction that Uses the ALT
Once the table exists on-chain and is populated, you build a v0 Versioned Transaction that references it.
Using @solana/web3.js:
import {
VersionedTransaction,
TransactionMessage,
AddressLookupTableAccount,
} from '@solana/web3.js';
async function sendV0Transaction(
payer: PublicKey,
instructions: TransactionInstruction[],
lookupTableAddress: PublicKey,
) {
const latestBlockhash = await connection.getLatestBlockhash();
// Fetch and deserialize the ALT
const lookupTableAccountInfo = await connection.getAddressLookupTable(
lookupTableAddress,
);
if (!lookupTableAccountInfo.value) {
throw new Error('Lookup table not found');
}
const lookupTableAccount = lookupTableAccountInfo.value as AddressLookupTableAccount;
// Build the v0 message with a lookup table
const messageV0 = new TransactionMessage({
payerKey: payer,
recentBlockhash: latestBlockhash.blockhash,
instructions,
}).compileToV0Message([lookupTableAccount]);
const tx = new VersionedTransaction(messageV0);
// Sign and send
// tx.sign([payerKeypair, ...otherSigners]);
const sig = await connection.sendTransaction(tx, { maxRetries: 3 });
return sig;
}
What’s happening here:
compileToV0Messagebuilds a v0 message instead of legacy.- You pass an array of
AddressLookupTableAccounts; the compiler:- Encodes which accounts are loaded via ALTs.
- Stores their indices into the message.
- At execution time, the validator:
- Reads the message’s lookup table section.
- Fetches the ALT from on-chain state.
- Resolves each 1-byte index back to a 32-byte public key.
- Executes your instructions against the full account list (inline + ALT-resolved).
Result: You can fit significantly more accounts—e.g., raising the 32-account ceiling up toward ~64 addresses per transaction—while keeping the packet within Solana’s byte limits.
Common Mistakes to Avoid
-
Using legacy transactions with ALTs:
Legacy transactions cannot use ALT-resolved addresses at runtime.
How to avoid it: Always compile to v0 (compileToV0Message) when you intend to use ALTs and wrap the message inVersionedTransaction. -
Assuming ALTs bypass all limits:
ALTs help with address list size, but you still face packet size, compute unit, and CPI depth limits.
How to avoid it: Treat ALTs as a way to reclaim bytes, not to ignore constraints. Monitor transaction size, CU consumption, and RPC errors. If your app feels slow or unreliable under load, fix your RPC patterns and transaction sizing before piling on more accounts.
Real-World Example
You’re building a multi-leg DeFi trade that:
- Reads prices from multiple oracles,
- Touches several token mints and user token accounts,
- Interacts with multiple liquidity pools and routing programs,
- Emmits memos for downstream reconciliation.
In a legacy transaction, listing every user account, pool account, oracle, PDA, and token account quickly pushes you up against the 32-account limit and packet size constraints. You’d be forced to:
- Split the route into multiple transactions,
- Add complex retry and reconciliation logic,
- Accept that any partial failure leaves you with operational cleanup.
With v0 and ALTs:
- You create one ALT per routing domain (e.g., “all AMM pool accounts”).
- You extend the table as pools and PDAs are added.
- Your routing engine builds a v0 transaction that:
- Lists only dynamic per-user accounts inline,
- References pool/oracle accounts via ALT indices.
The same route that previously required two or three transactions now fits in one, executing atomically, with funds secured in ~400ms and sub-cent fees. Your ops team gets simpler reconciliation (one signature per route), and your users see a single, fast confirmation instead of a multi-step sequence.
Pro Tip: Treat ALTs like you treat payment routing tables: stable, curated infrastructure. Don’t create and extend lookup tables on every user action. Pre-provision shared ALTs (e.g., for PDAs, pools, or merchant accounts), keep them under 256 entries, and cache them in your backend so your RPC layer isn’t re-fetching ALT accounts on every request.
Summary
v0 versioned transactions and Address Lookup Tables are how you break past the practical 32-account ceiling of legacy Solana transactions without fighting packet size on every build. ALTs store up to 256 addresses per table, referenced by 1-byte indices that the v0 message uses to “compress” your account list. To use them correctly, you:
- Create and extend ALTs on-chain,
- Fetch those tables in your backend,
- Compile v0 messages with lookup tables and send
VersionedTransactions.
The payoff is tangible: more complex, multi-account workflows in a single atomic transaction, better alignment with real payments and DeFi operations, and fewer reliability issues stemming from chained transactions and brittle retries.