All articles
Solana·June 8, 2026·5 min read

Address Lookup Tables: Fitting More Into One Solana Transaction

How Solana address lookup tables and versioned transactions compress account references to beat the 1232-byte transaction size limit.

Why a legacy transaction runs out of room

Every Solana transaction gets squeezed into a single UDP packet: 1232 bytes, derived from the 1280-byte IPv6 MTU minus 48 bytes of header overhead. That ceiling hasn't moved since mainnet launch, and it applies no matter how much your instruction actually needs to do.

The bulk of that budget goes to account references, not instruction data. A legacy transaction message lists every account it touches as a raw 32-byte public key, plus a 64-byte signature for each signer. Do the math on a real DeFi instruction: a multi-hop swap through Jupiter might touch 40-plus accounts once you count pool states, token vaults, mint accounts, oracle feeds, and the token program itself for each hop. At 32 bytes apiece that's 1,280 bytes just for the key list — over the limit before you've added a single byte of instruction data or a signature. This is why complex routes, multi-pool arbitrage, and anything touching more than roughly 30-35 accounts simply cannot fit as a legacy transaction, full stop.

What an address lookup table actually is

An Address Lookup Table (ALT) is a plain on-chain account, owned by the AddressLookupTableProgram, that stores up to 256 public keys in a flat list. Instead of writing a 32-byte pubkey into your transaction message, you write a 1-byte index into that list. The runtime resolves the index to the real address when it loads accounts for execution.

That's the entire trick. Nothing about execution changes — the program still receives the same AccountInfo structs in the same order. What changes is how expensive it is to reference an account inside the wire format of the transaction. A table costs you 32 bytes once (for the table's own address) plus 1 byte per account you pull from it, instead of 32 bytes per account.

Lookup tables only work with versioned transactions (message version v0), introduced alongside them. Legacy transactions have no concept of an address table section, so you can't retrofit an ALT onto an old-style message — you build the transaction as v0 from the start.

Building one: the actual mechanics

Creating and populating a table takes two instruction types, both on AddressLookupTableProgram:

import {
  AddressLookupTableProgram,
  TransactionMessage,
  VersionedTransaction,
} from "@solana/web3.js";

const slot = await connection.getSlot();

const [createIx, lookupTableAddress] =
  AddressLookupTableProgram.createLookupTable({
    authority: payer.publicKey,
    payer: payer.publicKey,
    recentSlot: slot,
  });

// max 30 addresses per extend call — batch larger sets
const extendIx = AddressLookupTableProgram.extendLookupTable({
  payer: payer.publicKey,
  authority: payer.publicKey,
  lookupTable: lookupTableAddress,
  addresses: poolAccounts, // up to 30
});

// send createIx + extendIx, then wait — see warmup below

const { value: lookupTableAccount } =
  await connection.getAddressLookupTable(lookupTableAddress);

const message = new TransactionMessage({
  payerKey: payer.publicKey,
  recentBlockhash: (await connection.getLatestBlockhash()).blockhash,
  instructions: [swapIx],
}).compileToV0Message([lookupTableAccount]);

const tx = new VersionedTransaction(message);

Run the byte comparison on that 40-account swap from earlier. Legacy: 40 × 32 = 1,280 bytes of keys alone. With one ALT covering those accounts: 32 bytes for the table address, 40 bytes for the indexes — 72 bytes total. You just freed over a kilobyte of your 1232-byte budget for instruction data, extra hops, or a second instruction entirely (like a Jito tip transfer bundled in the same transaction).

The gotchas that actually bite

  • Warmup delay. A table isn't usable in the same slot it was created or last extended. The runtime checks the table's last-extended slot against the slot hashes sysvar, so you need at least one confirmed slot to pass before referencing it. Fire-and-forget bots that create a table and immediately try to use it will get a "table not activated" style failure.
  • Signers can't live in a table. Only non-signing accounts (writable or readonly) can be compressed via lookup. The fee payer and any other signing account must stay in the static account keys section, still costing 32 bytes plus a signature. ALTs shrink the "supporting cast" of an instruction, not the signers driving it.
  • Deactivation has a cooldown. Closing a table to reclaim rent isn't instant — you deactivate it, then wait roughly 512 slots before it can actually be closed, so the runtime can be sure no in-flight transaction still references it.
  • Rent and reuse. A table costs rent proportional to its size, so the efficient pattern is a small number of long-lived, shared tables (one per DEX or per pool cluster you route through regularly) rather than a fresh table per transaction. Most serious market-making and arb infrastructure maintains a standing set of ALTs for the venues it trades against and just extends them as new pools get added.
  • 256-entry cap per table, but you can reference several tables in one transaction as long as the total message still fits in 1232 bytes and total loaded accounts stay within the runtime's execution limits.

Where this actually matters

This isn't an academic optimization. If you're running a Solana MEV / arbitrage bot that routes through three or four AMMs to close a price gap, or a market maker quoting across multiple pools simultaneously, ALTs are the difference between a transaction that fits and one that gets rejected at the RPC layer before it ever reaches a leader. The same applies to bundling logic for volume and bundle tooling on pump.fun-style launches, where you're often packing several swap instructions plus tips into one atomic unit.

It also interacts with how you get transactions to validators in the first place — table warmup timing is one more reason to reason carefully about Jito bundles versus plain priority fees when you're racing for inclusion. And if you're tracking table state or pool account changes in near real time to decide when to rebuild a lookup table, that's squarely Yellowstone gRPC territory rather than polling RPC. Validator client behavior around account loading and slot processing differs slightly between Firedancer and Agave too, which is worth knowing if you're chasing edge-case inclusion latency.

If your bot's instructions are hitting "transaction too large" on anything beyond a simple two-leg swap, it's time to build lookup tables into the transaction pipeline — talk to us about wiring this into your Solana MEV or arbitrage stack.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Solana#Address Lookup Tables#Versioned Transactions#MEV#Transaction Optimization