Originally published on xroot.dev.
Solana's transaction size limit — 1,232 bytes, unchanged since genesis — is being raised to 4,096 bytes. The catch: the extra room only exists inside a brand-new wire format, and the day its feature gate activates, code that merely reads transactions can start failing.
The upgrade ships as two proposals: SIMD-0296 raises the size ceiling, and SIMD-0385 defines the v1 transaction format that carries it. Legacy and v0 transactions keep working exactly as they do today — if you never touch v1, nothing about sending changes for you. But reading is a different story: one v1 transaction inside a block is enough to make an un-upgraded getBlock call fail outright, and indexers that scan ComputeBudget instructions will silently record zeros for every v1 transaction they see.
Here is what actually changes, why it changes, the three ways it breaks existing apps — loudly, silently, and sneakily — and the exact fix for each. Everything below is verified against the proposal texts, then exercised end-to-end against a local Agave 4.2 validator rather than taken from the headlines.
Why 1,232 Bytes Existed, and Why It Can Finally Move
The old limit was never a design goal — it was plumbing. Transactions originally travelled as single UDP datagrams, so they had to fit the minimum IPv6 MTU of 1,280 bytes minus 48 bytes of headers: 1,232 bytes for everything — signatures, accounts, instructions, data. Solana's networking has since moved to QUIC, whose specification (RFC 9000) imposes no explicit stream size limit. The physical reason for the cap is gone; SIMD-0296 removes it.
| Old limit (legacy / v0) | 1,232 bytes |
| New limit (v1 only) | 4,096 bytes (~3.3×) |
What never fit in 1,232 bytes: zero-knowledge proofs (Confidential Transfers), Winternitz one-time signatures, nested institutional multisigs, BLS and other non-precompiled signature schemes. Teams worked around it with Jito bundles — which are not atomic at the protocol level. The proposal picked 4,096 by measuring real bundle traffic (roughly half of all bundles fit in 2,048 bytes, 65% in 6,144) and then aligning to the 4 KiB memory page validators already manage, so a transaction never spans multiple pages.
Worth knowing: SIMD-0296 introduces no new per-byte fee. The expectation instead is that schedulers will price large transactions via priority fees — a bigger envelope will cost more to land, just not on a published curve.
v1 Is a New Wire Format, Not a Bigger v0
SIMD-0385 does not stretch the old envelope — it redesigns it. A v1 transaction announces itself with version byte 129 (0x81) at offset zero, and then diverges from v0 in four structural ways:
-
Resource requests become message fields. Compute unit limit, loaded-accounts data size, heap size, and priority fee move out of ComputeBudget instructions into a fixed-position
transactionConfigbitmask in the message itself. ComputeBudget instructions inside a v1 transaction still execute — as no-ops that burn compute units and configure nothing. - Address Lookup Tables are gone. All accounts are inline — up to 64 full 32-byte addresses, duplicates rejected at sanitization. Validators no longer load and deserialize ALT accounts before they can even parse a transaction.
- Fixed-width instruction headers. Each instruction's header (program index, account count, data length) is separated from its variable-length payload, so parsers compute instruction boundaries directly instead of walking the buffer sequentially.
- Signatures move to the tail. The message comes first, signatures last with no length prefix — the count is derived from the header.
The hard limits:
| Constraint | v1 limit |
|---|---|
| Transaction size | 4,096 bytes |
| Accounts | 64, inline only — no lookup tables |
| Instructions | 64 |
| Signatures | 12 |
| Accounts per instruction | 255 |
And the config fields that replace ComputeBudget instructions:
| Config field | Encoding | Default when unset |
|---|---|---|
| Priority fee | u64 LE, total lamports | 0 |
| Compute unit limit | u32 LE | 0 — not 200k/instruction |
| Loaded accounts data size | u32 LE | 0 — not 64 MiB (cost model floors at 32 KiB) |
| Heap size | u32 LE, 1 KiB multiples in [32 KiB, 256 KiB] | 32 KiB |
The unit change nobody will notice until their fee stats corrupt: v0 priority fees are micro-lamports per compute unit; the v1 priority fee is an absolute total in lamports. Any pipeline that averages, compares, or estimates fees across versions must normalize first — the raw numbers are not even the same dimension.
The ALT removal is a real trade-off, and it lands hardest on DeFi routing. About 62% of current v0 transactions reference at least one lookup table. Converted to inline addresses, half of them grow by under 420 bytes and 90% by under 1,400 — comfortably inside the new envelope. But dense multi-table routes expand by 1,500+ bytes, and the 64-account cap does not move, so broad multi-pool aggregator strategies stay account-bound, not byte-bound. (A draft proposal, SIMD-0596, would raise the cap to 96.)
The Part That Breaks Your App: Reading
The loud failure. The moment the gate activates, getTransaction, getBlock, and blockSubscribe return JSON-RPC error -32015 for anything v1 unless you pass maxSupportedTransactionVersion: 1. It is the exact rerun of the v0 migration of 2022 — with the same nasty amplification: one v1 transaction anywhere in a block fails the entire getBlock response, not just that transaction.
// Un-upgraded reader — fails with -32015 once any v1 tx lands in the block
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 0,
});
// Fixed — pass the integer 1 (the string "1" is rejected)
const block = await connection.getBlock(slot, {
maxSupportedTransactionVersion: 1,
});
This parameter is backwards compatible — you can and should ship it today, before activation. Responses for v1 transactions carry the new transactionConfig object inside the message; v0 and legacy responses are unchanged.
The silent failure. Every indexer I have seen extracts priority fees and compute limits by scanning for ComputeBudget instructions. Against a v1 transaction that scan finds nothing and returns zero — without an error. Your pipeline keeps running and quietly writes wrong fee data, wrong CU data, and fee estimates skewed by transactions that appear free. The fix: read transactionConfig when it is present, fall back to instruction scanning when it is not, and normalize the units before anything compares them.
The sneaky failure. Geyser and gRPC streams have no version gate at all — there is no parameter to reject v1, so the new transactions simply arrive. Yellowstone builds before 15.1.1 silently downgrade v1 to v0 on the wire, and protobuf stubs generated before yellowstone-grpc-proto 12.6.0 have no Message.config field to decode. Regenerate your stubs, and detect the version structurally: check for the presence of config on the message first, then the versioned flag — in that order, because a v1 message is also "versioned".
Sending v1: Zero Defaults and New Units
Nothing forces you to send v1 — legacy and v0 stay valid indefinitely. Adopt it when you need the bytes. When you do, the sharpest edge is that v1 resource limits default to zero. Legacy and v0 gave you 200k compute units per instruction and 64 MiB of loaded accounts for free; a v1 transaction that doesn't explicitly request a compute unit limit and a loaded-accounts data size fails at execution, after you have already paid to land it.
// @solana/kit ≥ 8.0.0 — every limit is explicit in v1
const message = pipe(
createTransactionMessage({ version: 1 }),
(m) => setTransactionMessageComputeUnitLimit(120_000, m),
(m) => setTransactionMessageLoadedAccountsDataSizeLimit(262_144, m), // 32 KiB pages
(m) => setTransactionMessagePriorityFeeLamports(10_000n, m), // TOTAL lamports, not per-CU
);
The working recipe: simulate with both limits maxed out, read the consumed values from the simulation, then set the real limits from the measurement — rounding the loaded-accounts size up to the next 32 KiB page, because the cost model floors it at 32 KiB anyway. Remember there are no lookup tables to lean on: every account is inline, 64 at most. And send with encoding: 'base64' — base58 encoding is hard-capped at 1,232 bytes, so a large v1 transaction cannot even be submitted through it.
Two audiences with homework even if they never build a v1 transaction:
- On-chain programs that introspect ComputeBudget. No sysvar exposes the v1 message config to on-chain code, and ComputeBudget instructions in v1 are no-ops — a program gating behavior on introspected compute budget simply cannot see it for v1 callers. Stop gating on it.
-
Fee sponsors and co-signers. A fee cap enforced by scanning ComputeBudget instructions does not bind a v1 transaction at all. Decode the raw bytes, check byte zero for
0x81, and read the limits fromtransactionConfigbefore you sign.
The Upgrade Matrix, and Where the Rollout Stands
Minimum versions that understand v1 — upgrading past these is the single highest-leverage preparation step:
| Library | Minimum version | Support |
|---|---|---|
@solana/kit (TypeScript) |
8.0.0 | read + send |
@solana/web3.js 3.x |
rc-0.3 (upcoming) | read + send |
@solana/web3.js 1.x |
1.99.0 (upcoming) | read only |
solana-* crates (Rust) |
4.2.x | read + send |
solders (Python) |
0.29.0 | read + send |
solana-go |
1.23.0 (or 2.0.0 on /v2) |
read + send |
yellowstone-grpc-proto |
12.6.0 | first stubs with Message.config
|
| Yellowstone geyser plugin | 15.1.1 | stops downgrading v1 → v0 |
@triton-one/yellowstone-grpc |
6.0.0 | decodes the config field |
As of late August 2026 the feature gate is not yet active on testnet, devnet, or mainnet. The format ships with Agave 4.2 — whose mainnet feature activations began rolling out the week of August 17 — and the Solana Foundation's stated expectation is mainnet activation within weeks. You can exercise v1 locally today with Solana CLI ≥ 4.2 or Surfpool ≥ 1.5, and check the gate yourself at any time:
solana -u m feature status txv1aq4pp281K9um3tnPgkfX8UqtFT6wcVW3hNezGLL
I ran both checks while writing this. The gate reports inactive on mainnet, testnet, and devnet as of August 28. And on a local CLI 4.2.1 test validator — where the feature is already live — a @solana/kit 8 transaction built exactly as in the sending section lands on-chain with byte zero 0x81, reads back as version 1 with its transactionConfig populated, and fails with -32015 the moment it is fetched with maxSupportedTransactionVersion: 0. The behavior in this post is reproduced, not paraphrased.
Runnable end-to-end examples in TypeScript, Rust, Go, and Python live in the Solana Foundation's transaction-v1-examples repository.
If You're Here Because Something Already Broke
Symptom to fix, in the order you are likely to hit them after activation:
| Symptom | Fix |
|---|---|
Error -32015 from getTransaction / getBlock / blockSubscribe
|
Add maxSupportedTransactionVersion: 1 — as an integer, not a string — to every call |
| Whole blocks failing to fetch | Same fix — one v1 transaction fails the entire getBlock response until the parameter is raised |
| Priority fees or CU limits suddenly reading zero | Your ComputeBudget instruction scan cannot see v1 — read transactionConfig when present, then fall back to scanning |
| Fee stats or estimates off by orders of magnitude | Unit mismatch: v0 is micro-lamports per CU, v1 is total lamports — normalize before aggregating |
| "Transaction too large" on send despite v1 | You are on base58 (capped at 1,232 bytes) — send with encoding: 'base64'
|
| v1 transactions failing with compute errors | Limits default to zero — explicitly set the compute unit limit and loaded-accounts data size |
| Geyser/gRPC stream shows v1 traffic as v0, or missing config | Upgrade Yellowstone plugin ≥ 15.1.1, regenerate protobuf stubs ≥ 12.6.0, detect on Message.config presence |
Bigger Envelopes, Sharper Edges
The upgrade itself is easy to like: ZK proofs, big multisigs, and heavyweight signature schemes finally land as single atomic transactions instead of bundle acrobatics, and validators get a format they can parse without touching state. But it is the first new transaction format since v0 in 2022, and the damage pattern will be the same — not the teams sending new transactions, but the readers who never opted in.
The preparation is cheap and safe to do today:
- Upgrade your SDKs past the matrix above.
- Ship
maxSupportedTransactionVersion: 1now — it is backwards compatible. - Audit anything that scans ComputeBudget instructions for fees or limits.
- Only then think about whether your own transactions want the extra 2,864 bytes.
Sources: the official Larger Transaction Sizes upgrade page, the SIMD-0296 and SIMD-0385 proposals, and Solana's ALT trade-off analysis.
I build and operate real-time Solana data pipelines — Geyser/gRPC ingestion, decoding, and analytics at firehose scale. If your indexer, wallet backend, or trading system needs to survive this migration, get in touch or read the pipeline series.
Top comments (0)