A multi-hop swap fails with a message about transaction size, and you have not even submitted it yet. The transaction is too large to serialize.
Solana caps a transaction at 1232 bytes. Every account you reference costs 32 bytes of that budget, and a routed swap through three pools references a lot of accounts.
Where the 1232 Bytes Go
The budget is consumed by four things:
signatures 64 bytes each
account keys ★32 bytes each★
instruction data varies
recent blockhash 32 bytes
★Account keys are usually what breaks you.★ A two-hop swap through an aggregator can reference thirty or more accounts — pools, vaults, token accounts, programs, authorities. At 32 bytes apiece that is most of your budget before any instruction data.
// Check before you try to send.
const serialized = tx.serialize();
console.log(`${serialized.length} / 1232 bytes`);
★This is a client-side failure.★ It happens during serialization, before anything reaches the network, which is why it looks different from every other failure in this space.
What a Versioned Transaction Changes
Legacy transactions list every account inline. Versioned transactions — v0 — can reference accounts through an address lookup table instead.
An ALT is an on-chain account holding a list of addresses. Your transaction references the table plus an index, ★which costs 1 byte per account instead of 32.★
import {
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
const lookup = await connection.getAddressLookupTable(tableAddress);
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message([lookup.value]); // ★the ALT goes here★
const tx = new VersionedTransaction(message);
tx.sign([payer]);
The saving is substantial. Thirty accounts inline is 960 bytes; through a lookup table it is closer to 30.
Creating a Lookup Table
Two steps, and they cannot be in the same transaction as the usage:
import { AddressLookupTableProgram } from "@solana/web3.js";
// 1. Create the table.
const [createIx, tableAddress] = AddressLookupTableProgram.createLookupTable({
authority: payer.publicKey,
payer: payer.publicKey,
recentSlot: await connection.getSlot(),
});
// 2. Extend it with addresses.
const extendIx = AddressLookupTableProgram.extendLookupTable({
payer: payer.publicKey,
authority: payer.publicKey,
lookupTable: tableAddress,
addresses: [pool, vault, tokenProgram, /* ... */],
});
★Two constraints that catch people:★
A table is not usable in the slot it was created. It needs to be confirmed first. Creating and using in one transaction fails.
Extending is also size-limited. You cannot add hundreds of addresses in one instruction — the extend transaction itself has to fit in 1232 bytes. Chunk it.
If your transactions serialize fine and the problem is them not landing, a free BoltTx key is one line to test the routing side against.
When ALTs Are Worth It
★Not always.★ The setup costs rent, two transactions, and operational complexity.
Worth it:
- Routed swaps through aggregators, which reference many accounts
- A bot trading a fixed set of pairs, where the same accounts recur
- Any transaction that currently fails on size
Not worth it:
- Simple transfers and single-pool swaps, which fit comfortably
- One-off transactions against accounts you will never touch again
- ★Sniping brand-new tokens★ — the accounts do not exist before the launch, so there is nothing to pre-populate
That last one matters for meme trading specifically. ★You cannot build a lookup table for a token that does not exist yet.★ For launches you are stuck with inline accounts and must keep the instruction set lean.
The Compute Cost
An often-missed detail: resolving a lookup table is not free.
The runtime has to read the table account and dereference each index. ★That consumes compute units on top of your actual instructions★, so a transaction that fits comfortably in compute without an ALT may need a higher limit with one.
// Simulate the version you will actually send, ALT included.
const sim = await connection.simulateTransaction(versionedTx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);
You are trading bytes for compute. Usually a good trade when you are near the size limit, and unnecessary overhead when you are not.
Legacy vs v0: What Else Differs
| Legacy | v0 | |
|---|---|---|
| Account references | inline, 32 bytes each | ★1 byte via ALT★ |
| Max accounts in practice | ~35 | ★~250+★ |
| Setup required | none | ★create + extend table★ |
| Compute overhead | none | table resolution |
| RPC support | universal | ★needs maxSupportedTransactionVersion★ |
★That last row causes silent problems.★ When reading transactions back, you must tell the RPC you understand v0:
// Without this, v0 transactions come back as null.
const tx = await connection.getTransaction(sig, {
maxSupportedTransactionVersion: 0,
});
Omit it and a v0 transaction that landed perfectly appears not to exist. ★This is a common false alarm — the transaction is fine, your read is wrong.★
Reducing Size Without an ALT
Before adding the machinery, three cheaper things:
Drop unnecessary accounts. Duplicate account keys are deduplicated automatically, but accounts you pass but never use still cost 32 bytes each.
Reduce hops. A three-hop route through an aggregator references far more accounts than a two-hop one. Sometimes accepting a slightly worse price is cheaper than the size overhead.
Split the transaction. Two transactions instead of one, if the operations do not need to be atomic. ★They do need to be atomic for arbitrage★, which is exactly when you cannot use this option.
Debugging Size Failures
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToLegacyMessage();
console.log("accounts:", message.accountKeys.length);
console.log("est. account bytes:", message.accountKeys.length * 32);
console.log("instructions:", message.instructions.length);
★If account bytes alone are over about 800, an ALT is the fix.★ If instruction data is what is large, an ALT will not help — reduce what you are asking the programs to do.
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★A size failure never even reaches submission★ — it fails on your machine, before submission. That makes it the cheapest failure in this space to fix, and the only one that costs you nothing when it happens.
Where BoltTx Fits
We handle submission. Versioned transactions submit exactly like legacy ones — the format affects serialization, not routing.
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, never sign, and never modify transaction contents.
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 the maximum Solana transaction size? 1232 bytes. It covers signatures, account keys, instruction data, and the blockhash. Account keys at 32 bytes each are usually what pushes a complex transaction over.
What is a versioned transaction on Solana? A transaction format (v0) that can reference accounts through an address lookup table instead of listing them inline. The reference costs about 1 byte instead of 32, which is what lets complex transactions fit.
What is an address lookup table? An on-chain account holding a list of addresses. A v0 transaction references the table plus an index rather than the full 32-byte address, dramatically reducing size for transactions with many accounts.
Why is my Solana transaction too large?
Almost always account count. A routed swap can reference thirty or more accounts at 32 bytes apiece. Count message.accountKeys.length — if account bytes alone exceed roughly 800, an ALT is the fix.
How do I create an address lookup table?
Two instructions: createLookupTable then extendLookupTable. The table cannot be used in the slot it was created — it must be confirmed first, so creation and use cannot share a transaction.
How many addresses can a lookup table hold? Up to 256. But you cannot add them all at once, because the extend transaction itself must fit within 1232 bytes. Chunk the additions across several transactions.
Do versioned transactions cost more compute? Yes, slightly. The runtime reads the table account and dereferences each index, which consumes compute on top of your instructions. Simulate the version you will actually send.
Should I always use versioned transactions? No. For simple transfers and single-pool swaps that fit comfortably, the setup cost and compute overhead buy you nothing. Use them when you are near the size limit.
Can I use a lookup table for sniping new tokens? No. The accounts for a brand-new token do not exist before the launch, so there is nothing to pre-populate. For launches you are limited to inline accounts and a lean instruction set.
Why does getTransaction return null for my transaction?
You probably omitted maxSupportedTransactionVersion: 0. Without it, the RPC will not return v0 transactions, so one that landed perfectly appears not to exist. The transaction is fine; the read is wrong.
What is the difference between legacy and v0 transactions? Legacy lists every account inline, capping you around 35 accounts. v0 can reference accounts through a lookup table, supporting far more. v0 requires table setup and adds compute overhead.
Can I reduce transaction size without a lookup table? Yes, three ways: remove accounts you pass but never use, reduce the number of hops in a route, or split into two transactions if the operations do not need atomicity. Arbitrage usually needs atomicity.
Does a lookup table need to be created by me? No. You can reference any existing table you know the address of. Aggregators often maintain public tables covering common accounts, which removes the setup cost entirely.
How do I know if an ALT will actually help? Compare account bytes against instruction data. If account keys dominate, an ALT helps a lot. If instruction data is what is large, an ALT changes nothing — you need to reduce what the programs are being asked to do.
Can I modify a lookup table after creating it? You can extend it with more addresses while you hold the authority, and you can deactivate and close it to recover rent. Existing entries cannot be replaced in place.
Does transaction size affect landing? Indirectly. An oversized transaction never gets sent, so it is a client-side failure rather than a landing failure. Once it serializes, size does not meaningfully change how it is routed.