Versioned transactions and address lookup tables
Solana transactions have a hard 1,232-byte size limit at the wire level. Legacy transactions put every account's full 32-byte pubkey directly in the message, so a 30-account transaction spends close to a kilobyte on pubkeys alone. The v0 transaction format introduced Address Lookup Tables: accounts that store pubkeys at fixed offsets, referenced from the transaction by a 1-byte index instead of a 32-byte pubkey. The same 30-account transaction now fits comfortably with hundreds of bytes to spare.
The 1,232-byte ceiling
A Solana transaction has to fit in a single UDP packet. The maximum packet size is 1,232 bytes, derived from the IPv6 minimum MTU minus protocol headers. That cap covers the entire transaction: signatures, header, account list, recent blockhash, and instruction data combined.
For simple transactions this is plenty. A wallet sending SOL touches three accounts and uses well under 200 bytes. A token transfer with an ATA touches five and uses around 300. But composing DeFi protocols stacks accounts quickly. A single DEX swap easily reaches ten accounts. Routing through three pools brings you to 25. Adding an oracle, a fee account, and a few sysvars puts you near 30, and each account costs 32 bytes.
Aggregators that route through multiple venues hit the wall hard. A multi-hop route can reference 40 or 50 accounts. Add the standard headers, signatures, blockhash, and instruction data, and you run out of room before the route is complete. Before versioned transactions existed, the workaround was to split the route across multiple transactions, which breaks atomicity and adds latency.
What v0 changed
The wire format gained a version prefix. A specific high-bit byte at the start signals "version 0," and everything after follows the v0 layout. Legacy transactions remain valid forever and follow the older layout without a prefix. The runtime distinguishes them by checking the first byte.
The new layout splits the account list into two parts. The static list contains signers, writable accounts that need to be in the message directly, and the program IDs being called. The second part is a set of references to Address Lookup Tables. Each reference identifies an ALT and lists indices into it, marking some as writable and others as readonly.
At execution time, the runtime resolves the references. It reads each referenced ALT, looks up the pubkeys at the specified indices, and assembles the full account list. Your program sees the same AccountInfo array it would have seen in a legacy transaction. The compression is invisible to the on-chain code. It is purely a wire-format optimization.
The practical ceiling jumps from roughly 30 accounts in a legacy transaction to around 256 in a v0 transaction. That covers any composition pattern Solana programs reasonably want to perform.
How Address Lookup Tables work
An ALT is an account owned by the Address Lookup Table program. It stores up to 256 pubkeys at fixed offsets. Anyone can create one and populate it with whatever pubkeys are useful.
The lifecycle has four steps. Create allocates the ALT and stamps it with the current slot, which becomes part of how the table is identified. Extend appends pubkeys to the table. Because the 1,232-byte ceiling also applies to the extend instruction itself, adding pubkeys takes multiple transactions to fill a large table, roughly 30 pubkeys per call. Freeze is optional and makes the table immutable. Close marks the table for deactivation. The rent is not released immediately. A cooldown of about 500 slots, roughly 5 minutes, runs before the close completes, which prevents the same address from being reused mid-transaction.
A newly created ALT cannot be used immediately. The runtime requires it to warm up for one slot before transactions can reference it. This protects against last-second changes that would alter the meaning of in-flight transactions.
A single transaction can reference up to four ALTs. Aggregators commonly pull from one ALT for token program IDs and common mints, one for the DEX they're routing through, and one for oracles or supporting accounts.
Signed accounts cannot come from an ALT. Signers always live in the static account list, because the runtime has to verify signatures before any account resolution happens.
What changes for you
For program authors, nothing changes. You write the same handlers, the same Accounts structs, the same CPIs. The runtime resolves ALT references before your handler runs, so by the time your code looks at ctx.accounts.foo, the resolution is already done.
For client authors, the change is meaningful. You build a VersionedTransaction instead of a Transaction, and you supply the list of ALTs you're referencing along with the instruction data. The @solana/web3.js SDK handles the encoding once you provide the right inputs. The harder part is deciding which accounts go into the static list versus which can be pulled from an ALT, and whether to create your own ALT or rely on someone else's.
What you actually do day to day
Most transactions you build don't need ALTs. They fit in a legacy transaction with room to spare. When you hit the size limit, the first response is to look at the account list and identify items that show up in every transaction your protocol issues: program IDs, common mints, shared authority PDAs, oracle accounts. Those are the right candidates for a protocol-specific ALT.
If you're building a protocol that other clients will compose with, publishing an ALT of your protocol's static addresses is a small investment that saves every integrator some bytes. Production Solana protocols often ship an ALT alongside their deployment for exactly this reason.
The mental model is short. A legacy transaction is a list of pubkeys. A v0 transaction is a list of pubkey references, some inline and some by ALT index. The runtime resolves the references before execution, and your program doesn't notice the difference.