Skip to content

Solana: /quote/v2 returns same-chain swaps whose transaction exceeds the 1232-byte packet limit #1103

Description

@Buidlor

Solana: /quote/v2 returns same-chain swaps whose transaction exceeds the 1232-byte packet limit — with maxRouteLength: 3 applied and zero client-added bytes

Hey team — same integrator as the Robinhood/DexAggregator issue. This one is the Solana "transaction too large" class. We now measure every failed swap at the moment of signing (analytic byte count from the compiled message, since web3.js refuses to even serialize the worst ones), so for the first time we can give you exact numbers instead of symptoms.

Short version: same-chain Solana quotes regularly compile to 1247–1481 bytes against the network's 1232-byte packet limit, through your SwapWidget, with your documented mitigation (maxRouteLength: 3) already applied to every request, and — important — with zero bytes added by us. Every transaction below is your quote exactly as returned.

Environment

  • relay-sdk ^5.2.6, relay-kit-ui ^7.1.6, relay-kit-hooks ^3.0.16, @relayprotocol/relay-svm-wallet-adapter ^18.0.6
  • All same-chain Solana (792703809 → 792703809), all via SwapWidget, tradeType: EXACT_INPUT
  • Our proxy injects maxRouteLength: 3 into every same-chain Solana /quote/v2 body (your Solana guide's mitigation, one notch stricter) — confirmed present in each captured request below
  • We previously appended two ComputeBudget instructions for priority fees (~52 B); since 2026-08-08 a size guard skips that injection entirely when the packet has no room. On every failure below the guard skipped — the byte counts are your transaction, untouched.

Fresh examples (production, 2026-08-08 → 09)

Byte counts measured from the compiled VersionedTransaction (1 signer). "serializes?" = whether @solana/web3.js could serialize it at all — it throws RangeError: encoding overruns Uint8Array once the message alone passes 1232.

Time (UTC) Mint (input) Request id Total bytes Over 1232 by Message bytes Keys / ix / ALTs serializes?
08-09 16:24:48 FjDHVCBaTVvvzQuA2YBJXk23pg7HAbNpW8gDbm8Qpump 0xe83497f81c36e9176ad6516470f189b38c75e9831836618b386b751c99a69f6c 1247 15 1182 16 / 6 / 5 yes (network rejects it)
08-09 14:07:06 7ssJZGFT3twGqeYA1kvpoMWwZYvMZvrMEbjRaWgg46BL 0x720a6b96f3af8a47e7b1db11a053848765a672cf0d14c543a6e2caff7640a174 1481 249 1416 23 / 5 / 6 no
08-09 11:41:55 Gea3tsKJ9pYwHamAR9CHmv5rqKH28epczdLtM3Brpump 0xa638317d2fa01ffe1907205e5d19a0ad7e4936703f728cca9d98d296ea5c833c 1309 77 1244 18 / 6 / 5 no
08-08 17:55:35 7ssJZGFT3twGqeYA1kvpoMWwZYvMZvrMEbjRaWgg46BL 0xa07051f538d4941052984ec2b6914f639fcc853ee9e5d668c878e21e4350df47 1340 108 1275 19 / 5 / 6 no

Quote age at failure: 1.4–5.2 s — these are fresh quotes, not stale ones. For the 1247-byte case we hold the exact signed-candidate transaction as base64 (deserializes byte-for-byte); for the others serialization is impossible, which is precisely the problem — the numbers come from the wire-format arithmetic of the compiled message, which we validate continuously against serialize() on transactions that do fit.

Two earlier reproductions on our staging build (2026-08-08, mint EjD5Y9NVhXmtEqU7wYvAyZvDWZFQeEuHXFatJmTbpump) measured 1278 and 1281 bytes — request ids 0x862bb48ddcddb6aa621ecf2e7ac534df50a9985b3ce2bb486e1899dc9f862905, 0x50c50f3c3ed532d1d7c6cd2d4d8257d686cc0ba94692b10a872e823d4b5933ed.

Anatomy of one failure (the 1481-byte case)

message: 1416 B  =  version 1 + header 3 + blockhash 32
                    + 23 static keys           737 B
                    + 5 instructions           392 B
                    + 6 address lookup tables  251 B   (23 writable / 23 readonly indexes)
signature: 65 B  →  total 1481 B  (limit 1232)

instructions:
  #0 ATokenGPvbdG… (ATA create)                        10 B
  #1 JUP6LkbZbjS1… (Jupiter, 43 accounts)              94 B
  #2 1111…1111    (System transfer)                    17 B
  #3 MemoSq4gqABA… (Memo)                              69 B   ← see below
  #4 DF1ow4tspfHX… (DFlow, 64 accounts, 133 B data)   201 B

Two things we'd flag from the shape:

  1. These routes pack two aggregator legs — Jupiter and DFlow — into one transaction, each dragging its own account set and lookup tables. That's where the bytes go; maxRouteLength: 3 doesn't bound it.
  2. Instruction Add correct chainId to execute status step #3 is a 69-byte Memo whose content is just the request id (e.g. 0x50c50f3c… in the staging repro — the memo text is the id, hex-encoded). It appears in every single failing transaction we've measured. On the margin cases it is literally the difference between a swap and a dead end: the 1247-byte failure lands at ~1178 without the memo and its program key. Could it be made optional, or dropped when the packet is tight? We can't use it anyway — these ids don't resolve (see below).

What the size check appears to miss

Two independent gaps, both visible in the numbers above:

  • Signatures. web3.js only refuses to serialize once the message exceeds 1232, but the network's limit includes the 65-byte signature section. A validator that mirrors serialization behaviour passes transactions up to ~1297 real bytes. The 1247-byte case fits that exactly: it serializes fine, and the RPC then rejects it (base64 encoded … too large: N (max: encoded/raw 1644/1232)).
  • Sometimes nothing at all. The 1481-byte quote has a 1416-byte message — over the limit before signatures even enter the picture. Whatever validation exists didn't apply to that route.

We've also seen your routing flap between two differently oversized routes for the identical input amount seconds apart (1246 B and 1349 B on mint B8dBBTAm3iZHu8ibvvGoSeqTJ69XEiXeJgsfWX5opump, 2026-08-08 ~17:34 UTC, request ids 0xd97f89ff…, 0x214bedc2…) — so re-quoting doesn't rescue the user; the button keeps arming and the swap keeps dying at signing.

What we've already applied on our side

  • maxRouteLength: 3 on every same-chain Solana quote (proxy-injected, covers the widget).
  • A re-quote ladder with includedOriginSwapSources: ["jupiter"] when a transaction comes back too large.
  • The priority-fee size guard: we never add our ComputeBudget instructions when they don't fit — confirmed inert on every case above.
  • Full disclosure: before that guard (shipped 2026-08-08), a minority of this class was our ~52 injected bytes pushing borderline quotes over — those are fixed and excluded here. Everything in this issue is over the limit with zero client additions.

What would fix it on your end

  1. Validate the full transaction — message + signatures — against 1232 before returning a quote, and make sure the check runs on every same-chain SVM route (the 1416-byte message suggests a path where it doesn't).
  2. Make the request-id Memo optional (a quote-body flag would do), or drop it automatically when the compiled transaction is within ~70 bytes of the limit.
  3. Return the compiled size (or the maxSizeBytes you validated against) in the quote response, so clients can reject a doomed route before asking the user to sign.
  4. Prefer a worse-price single-aggregator route over an unsignable dual-aggregator one when near the limit — "no route" is a better outcome than a swap that cannot be signed.
  5. None of the request ids above resolve in GET /requests/v3 ({"requests":[]}) — this class appears to be invisible in your own telemetry, which may be why it persists.

For any request id above we can provide the full /quote/v2 request and response and the per-instruction byte breakdown; for the serializable case, the exact base64 of the transaction we attempted to sign. We can also reproduce on demand — thin-liquidity pump.fun mints trigger it within a few attempts.

Thanks!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions