Очікує активації функції

Larger Transaction Sizes

Raising the maximum transaction size from 1232 to 4096 bytes

Поділитися:

червень 2026 р., автор: Solana Foundation

4096
Bytes per transaction, up from 1232
3.3x
More room for ZK proofs, multisigs, and batches

Updated August 2026 • Solana Foundation

A major upcoming upgrade to Solana raises the maximum transaction size from 1232 bytes to 4096 bytes. The size increase is defined in SIMD-0296 and delivered through the v1 transaction format introduced in SIMD-0385. It unlocks workloads that previously could not fit inside a single transaction, including ZK proofs, large multisigs, and some onchain signature schemes.

The existing v0 and legacy transaction formats continue to work as they do today, so applications and wallets that don't need the larger size will be unaffected. Applications that want to take advantage of it will need to update to v1 transactions.

Breaking Changes:

If your appBreaking?What you have to do
Reads transactions or blocksYesPass maxSupportedTransactionVersion: 1. Learn more
Indexes transactionsYesRead limits for v1 transactions from transactionConfig instead of from ComputeBudget instructions. Learn more
Sends transactionsNo (opt-in)If using v1 you MUST set the compute unit and data size limits explicitly (both default to zero). Learn more
Sponsors fees or co-signs someone else's transactionYesCaps that scan ComputeBudget instructions stop binding. Read the limits you gate on from transactionConfig. Learn more

Expected Mainnet Activation Date:

Anza's Agave v4.2 release schedule targets mainnet feature activation in Agave v4.2. This lands alongside the first stage of reduced slot times and a rent reduction. Anza's schedule is explicitly tentative and subject to change.

Current feature gate activation status:

КластерСтатус активації
TestnetАктивно
DevnetАктивно
MainnetНе активовано

If you want to check activation status of the feature gate yourself at any time, you can do so with the Solana CLI (use -u d or -u t for devnet or testnet):

solana -u m feature status txv1aq4pp281K9um3tnPgkfX8UqtFT6wcVW3hNezGLL

Local testing is available today. V1 Transactions are enabled in:

Support for V1 Transactions

These are the first releases that handle v1 transactions. Upgrade your dependencies to avoid errors.

LibraryMinimum supported versionv1 support
@solana/kit (Typescript)8.0.0Read and send
@solana/web3.js (Typescript) 3.x3.0.0-rc.3Read and send
@solana/web3.js (Typescript) 1.x1.99.0Read only — cannot build, sign, or send
solana-* crates (Rust)4.2.xRead and send
solders (Python)0.29.0Read and send
solana-foundation/solana-go (Go)1.23.0Read and send — also in 2.0.0 on the /v2 module path
yellowstone-grpc-proto (Rust)12.6.0First generated code carrying Message.config
Yellowstone geyser plugin (Rust)15.1.1Earlier builds downgrade v1 to v0 before the wire
yellowstone-grpc-client (Rust)12.0.0Latest is 13.3.0
@triton-one/yellowstone-grpc (Typescript)6.0.0Decodes field 7

Migration Checklist

For UsersNo action needed.+

Wallets and applications keep working exactly as they do today. The legacy and v0 transaction formats are unchanged, and nothing about how you approve, sign, or send a transaction changes.

What the upgrade buys you is headroom. Work that previously had to be split across several chained transactions — ZK proofs such as those used by Confidential Transfers, large multisigs, and batched operations — can land as a single atomic transaction. That means fewer signatures to pay for and one confirmation instead of several.

For DevelopersReading transactions is a breaking change. Sending v1 is opt-in.+

Start by upgrading dependencies (see Support for V1 Transactions), then work through whichever of these describes your application.

If you read or stream transactions — RPC consumers, indexers, explorers, Geyser pipelines (details):

  • When fetching data via HTTP RPC call, set maxSupportedTransactionVersion to the integer 1 on getTransaction and getBlock.
  • When streaming data via Websockets, set maxSupportedTransactionVersion to the integer 1 on blockSubscribe.
  • When streaming block data from Websockets via blockSubscribe, handle the block: null with an error as a failure, not an empty block.
  • Update raw transaction decoders to recognize the v1 129 (0x81) version prefix and parse its new layout.
  • Regenerate Geyser / gRPC protobuf stubs, then identify v1 from the presence of Message.config before checking the versioned flag.

If you introspect a transaction's compute budget or priority fee — indexers, onchain programs, relayers, paymasters, and anyone who sponsors a fee (indexing details, onchain details, sponsor details):

  • When parsing the V1 transaction payload, read transactionConfig instead of introspecting the ComputeBudget instructions.
  • Normalize priority fees before comparing across versions: v0 states a price in micro-lamports per compute unit, v1 a total in lamports.

If you want to send V1 transactions (details):

  • If you explicitly add ComputeBudget instructions, remove them from your transaction and use the messageConfig instead.
  • If you used Address Lookup Tables (ALTs) in V0, remove them from your transaction and place the accounts directly in the instruction (up to 64 accounts) instead.
  • Set compute unit limit and loaded accounts data size explicitly (the defaults are zero and your transaction will fail without them).
  • Estimate both from one simulation with both limits maxed; round the data size up to the next 32 KiB page.
  • Convert priority fees from micro-lamports-per-CU to total lamports.
  • Pass encoding: 'base64' when simulating and sending.

The technical details below explain each of these in full. Sample code is available here.

GitHub: Transaction V1 Example Code has a runnable example in each of these languages — sending, decoding, reading blocks, and indexing over gRPC — against a local validator, plus a version matrix recording the first release of every dependency that handles v1, including the ones that drop the config silently rather than erroring. Details for all breaking changes are included below:

For Transaction ReadersOpt in to v1 transactions before they appear onchain.+

Breaking Change: Reading Transactions and Blocks

Once v1 transactions appear onchain, any RPC consumer that has not opted in starts failing. The fix is one line:

  • Pass maxSupportedTransactionVersion: 1 on getTransaction and getBlock. It must be the JSON integer 1, not the string "1".
  • Passing 0 or legacy fails on v1 transactions exactly like omitting the parameter.
  • The parameter declares the highest version your client can decode, NOT a ceiling and NOT a request for a particular version. Raising it to 1 does not change how legacy and v0 transactions are returned.

How each read surface behaves without the opt-in:

MethodWithout maxSupportedTransactionVersion: 1
getTransactionv1 Transaction fails with error -32015
getBlockOne v1 transaction fails the entire block — no partial result
blockSubscribeEmits block: null and stops advancing — wedges on the first v1 slot
getSignaturesForAddressNot impacted — v1 signatures list normally,

Opted-in responses carry a new transactionConfig object inside the message for v1 transactions (it is absent entirely for legacy and v0):

"message": {
  "instructions": ["… no ComputeBudget instruction here …"],
  "recentBlockhash": "GsdgFbNBoZmAB5uPHfk2xUFYyM4Wg2hYZBfBrxrqjxfF",
  "transactionConfig": {
    "computeUnitLimit": 30000,
    "heapSize": null,
    "loadedAccountsDataSizeLimit": 200000,
    "priorityFee": null
  }
}
For IndexersDecode v1 transaction configuration instead of ComputeBudget instructions.+

Breaking Change: Indexing Transactions

ComputeBudget scanning returns nothing. Any pipeline that derives priority fees or compute unit limits by scanning instructions for the ComputeBudget program will report zero for every v1 transaction, without erroring. In v1 those values live in transactionConfig. This is the most likely path to quietly wrong analytics rather than a loud failure.

Geyser / gRPC streams have no version gate at all. There is no version field in the protobuf. A stale consumer does not error on v1; it silently misreads it as v0 with an empty compute budget. Regenerate your protobuf stubs and detect the version structurally on Message.config, field 7, checking in this order:

Check, in this orderVersion
config field presentv1
config absent, versioned truev0
neitherlegacy

The order matters because the versioned boolean is true for both v0 and v1. Testing it first classifies every v1 transaction as v0 — silently, with a zeroed compute budget.

In TypeScript:

function messageVersion(message: Message): "legacy" | "v0" | "v1" {
  if (message.config !== undefined) {
    return "v1";
  }
  return message.versioned ? "v0" : "legacy";
}

And in Rust:

fn message_version(message: &Message) -> MessageVersion {
    match (&message.config, message.versioned) {
        (Some(_), _) => MessageVersion::V1,
        (None, true) => MessageVersion::V0,
        (None, false) => MessageVersion::Legacy,
    }
}

config is a submessage, and submessage fields always carry explicit presence in proto3 — so this check holds however your decoder handles defaults, and whether or not your copy of the schema marks the field optional.

Comparing fees across versions needs normalising. v0 states a priority fee as a price in micro-lamports per compute unit; v1 states a total in lamports. To put both in one dashboard, multiply the v0 price by the compute unit limit the transaction actually requested, then divide by 1,000,000:

20,000 CU × 250,000 micro-lamports/CU = 5,000 lamports   // v0
                                        5,000 lamports   // the v1 equivalent
For Transaction SendersSending v1 is opt-in, but its resource limits must be explicit.+

Sending v1 Transactions

In legacy and v0, setting a resource limit is optional as the runtime provides default fallback values. In v1, omitting it sets the resource limits to zero — and a transaction that cannot run.

Unset fieldlegacy / v0v1
Compute unit limit200k per ix, max 1.4M0 CU
Loaded accounts data size64 MiB0 bytes
Heap size32 KiB32 KiB

A v1 transaction with an empty config fails at account loading with MaxLoadedAccountsDataSizeExceeded, meaning you requested a budget of insufficient bytes. This means: always set the compute unit limit and loaded accounts data size limit explicitly. Simulate once with both limits maxed, read unitsConsumed and loadedAccountsDataSize from the result, and write those into the config (rounding the data size up to the next 32 KiB page, since the block cost model charges in 32 KiB pages and headroom below the next page boundary is free).

Size your data budget with headroom for accounts that do not exist yet. Loading an account is charged 64 bytes of base metadata plus its data length, while an account that does not exist is charged nothing at all, so creating one is a step change from 0 to at least 64 bytes. If an account is created between your simulation and your send, a data size limit measured exactly against simulation can tip over into MaxLoadedAccountsDataSizeExceeded once the transaction reaches the network.

limit written by the estimator: 149at estimaterecipient absentfee payer · 64System program · 85recipient · 0149  lands exactly on the limit — passeson replayrecipient now existsfee payer · 64recipient · 64System program · 85213total crosses here, while loading the System program— not while loading the recipient that caused the growth
A SOL transfer seeding a brand-new wallet. Loading happens before execution, so the recipient is still absent at estimate time. The check is > rather than >=, so 149 lands exactly on the limit and passes.

Three more sending changes:

  • Priority fee is now a total, not a price. v0 sets micro-lamports per compute unit; v1 sets an absolute total in lamports. Do not carry the per-CU multiplication across.
  • ComputeBudget instructions become no-ops. They are neither parsed nor rejected — they execute successfully while doing nothing, burning 150 compute units and an instruction slot. Strip them.
  • Use base64 encoding for large transactions. Submitting over 1232 bytes requires encoding: "base64". base58 stays capped at 1232 bytes regardless of transaction version, and was deliberately not raised on deprecation grounds, so the 4096-byte ceiling is only reachable over base64.
For Program DevelopersA program cannot read a v1 transaction's compute budget or priority fee.+

Breaking Change: Onchain Introspection of the Compute Budget

A program reads the compute budget today by scanning the Instructions sysvar for ComputeBudget instructions. In v1 those values live on the message, and nothing exposes the message config to onchain code: the Instructions sysvar carries instructions only, no sysvar holds the config, and no syscall returns it.

ComputeBudget instructions in a v1 transaction are not rejected; they execute as no-ops, meaning their internal set values are no longer reliable. At present there is no sysvar to determine a transaction's version or to extract the message config. This means that program should stop gating on introspected ComputeBudget Instructions once v1 transactions are live.

We are evaluating additional options. Check back here for updates.

For Paymasters and Server SignersA fee cap that scans instructions does not bind a v1 transaction.+

Breaking Change: Sponsoring and Co-Signing Transactions

If you co-sign, sponsor the fee for, or verify a transaction somebody else built (e.g., relayers, paymasters, and gasless backends):

  • Detect the version before you gate on anything. Unlike a program, a server can always tell what it is holding. Decoding the base64 a payer hands you yields the version directly the message's first byte is 0x81 for v1.

  • For v1 transactions, read the priority fee and compute limits from transactionConfig, not from ComputeBudget instructions. Priority fees are a total in lamports, not micro-lamports per CU. The same decode that gave you the version gives you the config. Note: no-op ComputeBudget instructions can exist on a V1 instruction. These have no relevance to actual v1 resources requested. They should be disregarded.

  • Pass maxSupportedTransactionVersion: 1 on getTransaction and getBlock. It is a ceiling, not a suggestion: leave it at 0 and reading back a v1 transaction fails with -32015 while the transaction itself is onchain.

SDK Cheat Sheet

Use these APIs to build, send, and read v1 transactions.

TypeScript — @solana/kit@solana/kit 8.0.0 or later+
TaskAPI
Build a v1 messagecreateTransactionMessage({ version: 1 })
Set resource limitssetTransactionMessageComputeUnitLimit
setTransactionMessageLoadedAccountsDataSizeLimit
setTransactionMessageHeapSize
setTransactionMessagePriorityFeeLamports
Reserve limit space before simulatingfillTransactionMessageProvisoryResourceLimits
Measure limits by simulationestimateResourceLimitsFactory
Write measured limits backestimateAndSetResourceLimitsFactory
Decode a transaction off the wiregetTransactionDecoder
getCompiledTransactionMessageDecoder
decompileTransactionMessage
Read the configmessage.config on the v1 arm of TransactionMessage
Opt in to reading v1maxSupportedTransactionVersion: 1 on getTransaction / getBlock

The compute unit limit, loaded accounts data size limit, and heap size setters work for messages of any version. Priority fees differ: v1 stores a total in lamports, while legacy and v0 use micro-lamports per compute unit. Use setTransactionMessagePriorityFeeLamports for v1 and setTransactionMessageComputeUnitPrice for legacy and v0; Kit enforces this distinction with its types.

Rust — solana-*solana-* 4.2.x+
TaskAPI
Build a v1 messagev1::Message::try_compile_with_config
Set resource limitsv1::TransactionConfig::empty()
.with_compute_unit_limit(…)
.with_loaded_accounts_data_size_limit(…)
.with_heap_size(…)
.with_priority_fee(…)
Reserve limit space before simulating
Measure limits by simulationRead unitsConsumed and loadedAccountsDataSize from simulateTransaction.
Write measured limits back
Decode a transaction off the wireVersionedTransaction deserialization, or EncodedTransaction::decode on an RPC response
Read the configVersionedMessage::V1(m) => m.config
Opt in to reading v1max_supported_transaction_version: Some(1) on RpcTransactionConfig / RpcBlockConfig

Full sample code for TypeScript, Rust, Go, and Python are available on GitHub.


Technical Details

Size Limit

The new per-transaction size limit is 4096 bytes, up from 1232. The previous limit was set by Solana's conservative use of a 1280-byte MTU (Maximum Transmission Unit), which after overhead left 1232 bytes for transaction payload.

In 2022, QUIC became the default transaction ingestion protocol, which does not specify a maximum stream size, so the maximum transaction size can be raised.

The ceiling stays at 4096 rather than going larger as this matches the standard 4 KiB memory page used by validator hardware, which keeps per-transaction memory handling cheap and avoids forcing a single transaction to span multiple pages.

Once a transaction exceeds the MTU, it has to be split across multiple QUIC frames, and a single dropped frame triggers a retransmit of the whole set. This makes larger transactions more expensive to receive reliably and puts more pressure on validator in-flight buffers. Keeping the transaction size under 4096 avoids this problem.

A 4096-byte ceiling covers a meaningful share of the use cases that have relied on bundles, letting developers land them as individual atomic transactions instead. Because larger transactions consume more validator bandwidth, the scheduler is expected to require higher priority fees to land them than smaller transactions of equivalent priority.

v1 Format

The larger size is only available in the v1 transaction format, specified in SIMD-0385.

v1 reorders the transaction envelope. Signatures move to the tail and the transaction discriminator moves to the first slot (offset zero), letting infrastructure identify the format without deserializing anything. For v1, that discriminator is the byte 129 (0x81).

byte 0legacySignaturesHeaderBlockhashAccountsInstructionsv0Signatures0x80HdrBlockhashAccountsInstructionsALTsv10x81HdrMaskLifetimeCntsAddressesValuesIxSignaturessignatures move to the tail — no length prefixu32 bitmaskfixed-width values
v1 puts 0x81 at byte 0 and appends signatures last with no length prefix — the count is implied by the header. The config mask and its values are new.

The config mask and values are new: compute unit limit, loaded accounts data size limit, heap size, and priority fee live at fixed positions in the message instead of inside ComputeBudgetProgram instructions. This means the network can rank a transaction by priority fee with a single fixed-offset read instead of scanning and deserializing its instruction list.

The limits also change. v1 transactions can be larger but must be flatter:

Limitlegacyv0v1
Transaction size1232 bytes1232 bytes4096 bytes
Account addresses~32, size-bound64, via lookup tables64, inline
Address lookup tablesnot supportedsupportednot supported
Duplicate addressesallowedallowedrejected

The legacy and v0 numbers above are runtime limits, not format limits. Both formats can encode more accounts than they can execute — the runtime rejects any transaction locking more than 64 accounts. Legacy transactions cannot reach even that: 64 inline addresses at 32 bytes each is 2048 bytes, so a legacy transaction runs out of its 1232-byte budget somewhere around 32 addresses, depending on how many signatures and how much instruction data it carries. v0 reaches the full 64 by referencing lookup-table addresses at 1 byte each instead — which is the whole point of lookup tables. v1 reaches the same 64 with plain inline addresses, because 2048 bytes fits comfortably inside 4096, and makes 64 an explicit format limit checked at sanitization — so dropping lookup tables costs nothing in practice. A draft proposal, SIMD-0596, would raise the account limit to 96.

The larger size enables transaction-level workloads that did not fit inside 1232 bytes, including ZK proofs such as those used by Confidential Transfers, Winternitz one-time signatures, nested multisigs often used by institutions, and signature schemes like BLS.

About This Upgrade

Larger transaction sizes is a significant usability improvement for Solana developers. By raising the per-transaction byte limit to 4096 bytes, this upgrade lets developers land previously oversized workloads as single atomic transactions instead of stitching them together with address lookup tables or Jito bundles. In practice this also lowers cost and latency - there are fewer signatures to pay and one single confirmation rather than several chained transactions.

This upgrade addresses a practical ceiling that application teams have had to design around since the early days of Solana, and brings a whole new class of capabilities that we're looking forward to support.

The format details are specified in SIMD-0385 and the size increase in SIMD-0296. See also developer documentation on versioned transactions and transaction-v1-examples for runnable code in Rust, TypeScript, Python, and Go.

Learn more: Solana Upgrades