All articles
solanatransactionsaltversioned-transactions

Address Lookup Tables on Solana: when they earn back their cost

Address Lookup Tables let a transaction reference 64 accounts in 1 byte instead of 32 each. Here's exactly when they earn back their cost.

A Solana transaction has a hard ceiling: 1232 bytes. Inside that envelope you have to fit instructions, signatures, the recent blockhash, and — most expensively — the list of accounts the transaction touches. Each account is a 32-byte public key.

For a simple SOL transfer that's no problem. For a Jupiter swap that hits three pools and four token programs, you blow the limit almost immediately. Address Lookup Tables (ALTs) are the workaround: a versioned-transaction feature that lets you reference accounts by a 1-byte index into a pre-published table, instead of inlining all 32 bytes of the key.

How they work

You publish a table on-chain that holds up to 256 addresses:

typescript
import {
  AddressLookupTableProgram,
  Connection,
  Keypair,
  TransactionMessage,
  VersionedTransaction,
  sendAndConfirmTransaction,
  Transaction,
} from "@solana/web3.js"

// 1. Create the table
const slot = await connection.getSlot()
const [createIx, lookupTableAddress] = AddressLookupTableProgram.createLookupTable({
  authority: payer.publicKey,
  payer: payer.publicKey,
  recentSlot: slot - 1,
})

// 2. Extend it with the accounts you'll reuse a lot
const extendIx = AddressLookupTableProgram.extendLookupTable({
  payer: payer.publicKey,
  authority: payer.publicKey,
  lookupTable: lookupTableAddress,
  addresses: [pool, mintA, mintB, tokenProgram, ammProgram, /* ... */],
})

await sendAndConfirmTransaction(connection, new Transaction().add(createIx, extendIx), [payer])

Once the table is on-chain (and at least one slot has passed — a consistency requirement), you build versioned transactions that reference it:

typescript
const lookupTableAccount = (await connection.getAddressLookupTable(lookupTableAddress)).value!

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

const tx = new VersionedTransaction(messageV0)
tx.sign([payer])
await connection.sendTransaction(tx)

The compiler walks your instructions, sees which accounts appear in the lookup table, and replaces the 32-byte keys with 1-byte indexes plus a 32-byte reference to the table itself.

The actual math

For each address in your transaction that's also in an ALT:

  • Without ALT: 32 bytes (full pubkey in account list)
  • With ALT: 1 byte (index) + amortized fraction of the 32-byte table reference

The table reference is fixed overhead per ALT used in a transaction (~33 bytes). So the breakeven is roughly:

text
Bytes saved = (N × 31) − 33

Where N = number of accounts in your tx that are also in the ALT.

N = 1 → save -2 bytes (you lose)
N = 2 → save 29 bytes
N = 3 → save 60 bytes
N = 10 → save 277 bytes
N = 30 → save 897 bytes

If you can fit 3+ shared accounts in an ALT, you start coming out ahead per transaction. With 10+ — typical for AMM / aggregator paths — the savings are dramatic.

The setup cost

Creating and extending an ALT costs rent (~0.0027 SOL for the table itself plus a few thousand lamports per address) and a couple of transactions. The table is also frozen for 256 slots after creation before it can be used — a consistency window. You can't create an ALT and use it in the same transaction.

Once you've eaten the setup cost, the table is reusable forever. Every subsequent transaction that uses it saves bytes for free.

When to use them

Always use ALTs when:

  • You're building a DEX aggregator, perp DEX, or anything that hits many programs/pools per transaction
  • You operate a hot program where the same accounts show up in every transaction (oracle, treasury, your program ID, system program, token program, ATA program)
  • You're bumping against the 1232-byte limit and need to fit one more instruction

Skip ALTs when:

  • Your transaction touches <3 reusable accounts (you're paying overhead for no gain)
  • The transaction is genuinely one-shot — a user sending SOL once will never benefit from a lookup table
  • You can't commit to keeping the ALT's authority stable — if you frequently extend or migrate tables, the byte savings get eaten by table management overhead

The Jupiter pattern

Jupiter maintains a small set of canonical ALTs containing the most frequently-hit Solana program and pool addresses. Any quote response from the Jupiter API ships with the ALT addresses to include when building the swap transaction. That's the reason a complex cross-pool swap fits inside a single Solana transaction at all.

If you're routing through Jupiter, you're already using ALTs — you just don't see it. If you're writing your own router or aggregator, treat ALT publishing as part of the deploy pipeline, not a future optimization.

References

ALTs aren't magic — they're a byte-accounting trick that pays off once you reuse the same accounts often enough. Most apps that bothered measuring found the breakeven sooner than they expected.

Address Lookup Tables on Solana: when they earn back their cost | devrels.xyz