Sending multiple transactions

Plan and execute work across several transactions

When an operation cannot fit in a single transaction — too many instructions, too much data, or work that should run in parallel — you need to split it across several transactions. Kit clients support this through the same client.sendTransactions(...) and client.planTransactions(...) methods you used in Sending transactions, but plural.

Prerequisites

This guide assumes a transaction-ready client, like the one set up in Sending transactions. The same bundle plugins from @solana/kit-plugin-rpc and @solana/kit-plugin-litesvm install everything client.sendTransactions(...) needs out of the box.

Instruction plans

An instruction plan describes an operation made of several instructions together with constraints on how they can run — some steps must happen sequentially, others may run in parallel, and some must stay atomic. Kit represents this as a tree, so simple operations stay simple while more complex flows can nest sequential and parallel branches inside each other. The plan is then handed to a transaction planner that decides how to pack it into transaction messages, and then to a transaction plan executor that sends those transactions.

Kit's planners can divide work, batch it into transactions, and even pack variable-sized data into the available space. For most cases you only need to know about three building blocks: singleInstructionPlan, sequentialInstructionPlan, and parallelInstructionPlan. The Instruction plans advanced guide goes much deeper.

Run work sequentially

Use sequentialInstructionPlan(...) when each step depends on the previous one — for example, creating an account before initializing it.

const  = ([, ]);

Sequential plans accept either raw instructions or other instruction plans, so you can compose larger flows from smaller ones. A nonDivisibleSequentialInstructionPlan(...) variant also exists for steps that must run atomically inside a single transaction (or transaction bundle when not possible).

Run work in parallel

Use parallelInstructionPlan(...) when independent steps can run in any order. The planner is then free to pack them into separate transactions and send them concurrently.

const  = ([, ]);

The two helpers compose freely: a sequential plan can contain parallel children and vice versa. This is how you describe operations like "set up these two accounts in parallel, then run the operation that depends on both of them".

Plan multiple transactions

When you only want to inspect or persist the planned transactions, client.planTransactions(plan) runs the planner without executing anything and returns the resulting TransactionPlan tree.

import { ,  } from '@solana/kit';
import {  } from '@solana-program/system';
const  = ([
    ({
        : .,
        : ,
        : (500_000n),
    }),
    ({
        : .,
        : ,
        : (500_000n),
    }),
]);
 
const  = await .();

The returned TransactionPlan keeps the same sequential and parallel structure as the original instruction plan, but its leaves are the concrete transaction messages the planner chose to pack the work into. This is useful for dry runs, debugging, or feeding a custom executor.

Send multiple transactions

To plan and send in one call, use client.sendTransactions(plan). The result is a TransactionPlanResult tree that mirrors the original plan, with one leaf per transaction.

import { ,  } from '@solana/kit';
import {  } from '@solana-program/system';
const  = ([
    ({
        : .,
        : ,
        : (500_000n),
    }),
    ({
        : .,
        : ,
        : (500_000n),
    }),
]);
 
const  = await .();

client.sendTransactions(...) is the multi-transaction counterpart of client.sendTransaction(...). The single-transaction version asserts that the plan resolves to exactly one transaction and unwraps its result — a convenience for the common case. The plural version preserves the full tree, which you can walk to see what happened.

Understand transaction plan results

Every leaf in the result tree is a SingleTransactionPlanResult with one of three statuses:

  • successful — the transaction was sent and confirmed; context.signature is always present.
  • failed — the transaction was attempted but failed during preflight, simulation, or execution. error carries the underlying error.
  • canceled — the transaction was never attempted, typically because a prior transaction in a sequential branch failed first.
if (. === 'single') {
    if (. === 'successful') {
        .('Signature:', ..);
    } else if (. === 'failed') {
        .('Failed:', .);
    } else {
        .('Canceled before being sent');
    }
}

Branch nodes use kind: 'parallel' or kind: 'sequential', with their child results in plans. You usually do not need to walk the tree by hand — the helpers in the next sections cover the common cases.

Summarize results

summarizeTransactionPlanResult(...) flattens the tree and bucketizes the results so you can quickly check whether everything succeeded.

const  = ();
 
if (.) {
    .(`✅ ${..} transactions confirmed`);
} else {
    .(
        `${..} ok, ` +
            `${..} failed, ` +
            `${..} canceled`,
    );
}

This is usually the first thing to reach for after client.sendTransactions(...) — it gives you a quick health check without writing any tree-walking code.

Continue after failures

By default, client.sendTransactions(...) throws as soon as any transaction in the plan fails. That behaviour is convenient for small operations, but counterproductive when you are running a large batch and want to inspect partial results. The passthroughFailedTransactionPlanExecution(...) helper turns that thrown error back into a TransactionPlanResult.

import { ,  } from '@solana/kit';
const  = {} as ;
const  = await (
    .(),
);

After this call, result is the same tree you would have received on full success — but with failed and canceled leaves where the issues occurred. You can then summarize it, walk it, or react however you need.

Handle partial success

When you want to react to each transaction individually, flattenTransactionPlanResult(...) collapses the tree into an array of leaf results in the order they appear.

for (const  of ()) {
    if (. === 'successful') {
        .('✅', ..);
    } else if (. === 'failed') {
        .('❌', ..);
    } else {
        .('⏭️', 'canceled');
    }
}

Combined with passthroughFailedTransactionPlanExecution(...), this is enough to drive a UI that shows per-transaction status, retry buttons, or detailed error messages for a long-running multi-transaction operation.

Next steps

On this page