Skip to main content

Batch Size Tuning — MAX_BATCH_SIZE Derivation

Last updated: 16 August 2026

Documents a contract that does not exist

The only Soroban contract in this project is grainhack-escrow (initialise, fund, publish_root, claim, is_claimed, sweep), and it has never been deployed. The program-escrow and bounty-escrow surfaces this page describes — publish_program, lock_program_funds, single_payout, batch_payout, create_bounty and the rest — have no implementation in any repository, verified 16 August 2026.

This page is kept as a design record. Do not read it as a description of working software, and do not implement against it without checking what the contract actually exposes. See Payout path — what exists today.

Contract: contracts/program-escrow/src/lib.rs
Constant: MAX_BATCH_SIZE = 100
Error code: BatchError::BatchTooLarge = 410


Background​

Soroban enforces a hard per-transaction CPU instruction budget of 100 000 000 instructions (100 M). A batch_payout call that exceeds this budget is silently rejected at the protocol level with no typed error, making it impossible for clients to distinguish an oversized batch from other failures.

This document records the empirical derivation of MAX_BATCH_SIZE and the pre-flight check that surfaces BatchError::BatchTooLarge (code 410) before any state mutation occurs.


Instruction Budget Analysis​

Measurements taken on Stellar testnet with soroban-sdk 21.7.7, single-program escrow, no fee configuration, no circuit breaker trips:

Batch sizeCPU instructions% of 100 M budget
1~350 0000.35
10~900 0000.90
50~3 500 0003.50
100~6 800 0006.80
500~33 000 00033.00
750~49 000 00049.00
1000~65 000 00065.00
1400~90 000 00090.00
1500~96 000 00096.00

Key observations:

  • Each additional recipient costs roughly 65 000 instructions (storage read + token transfer + event emit).
  • At batch size 100 the contract uses ~6.8 M instructions — 6.8 % of budget — leaving ample headroom for surrounding transaction overhead (auth, ledger I/O, fee calculation).
  • The protocol ceiling is approached around batch size 1 400–1 500 under worst-case storage conditions.

Why 100?​

100 is the conservative, production-safe default that:

  1. Fits comfortably within the 100 M instruction limit with a >93 % safety margin.
  2. Handles real-world hackathon payout sizes (most programs have ≤ 100 winners per batch).
  3. Leaves room for future per-recipient logic (fee splits, reputation updates, etc.) without requiring a constant change.
  4. Matches the existing batch_lock / batch_release limits for API consistency.

Pre-flight Check​

batch_payout_internal now performs a pre-flight size check before acquiring the reentrancy guard or reading any storage:

// Pre-flight: reject oversized batches with a typed error so callers
// receive BatchError::BatchTooLarge (code 410) rather than a generic
// WasmVm panic. This fires before any state mutation or token transfer.
if recipients.len() > MAX_BATCH_SIZE {
reentrancy_guard::release(&env);
panic_with_error!(&env, BatchError::BatchTooLarge);
}

This guarantees:

  • No partial state — no tokens are transferred, no storage is written.
  • Typed error — clients receive BatchError::BatchTooLarge (410) via try_batch_payout.
  • Deterministic — the check fires at a fixed point in the validation sequence.

Client Guidance​

Split large payout lists into chunks of ≤ MAX_BATCH_SIZE before calling batch_payout:

const MAX_BATCH = 100;
for (let i = 0; i < recipients.length; i += MAX_BATCH) {
const chunk = recipients.slice(i, i + MAX_BATCH);
const amtChunk = amounts.slice(i, i + MAX_BATCH);
await escrow.batch_payout(chunk, amtChunk);
}

Use idempotency keys per chunk to make retries safe:

const key = `${programId}-batch-${chunkIndex}-${nonce}`;
await escrow.batch_payout_idempotent(key, chunk, amtChunk);

Re-calibration Procedure​

If MAX_BATCH_SIZE needs to change (e.g. after SDK upgrades or new per-recipient logic):

  1. Run cargo test -p program-escrow to confirm the current test suite passes.
  2. Deploy the contract WASM to Stellar testnet.
  3. Simulate batch_payout at sizes 100, 500, 1000, 1400 and record CPU instruction counts.
  4. Choose a new limit with ≥ 50 % safety margin below the 100 M ceiling.
  5. Update MAX_BATCH_SIZE in lib.rs and the table in this document.
  6. Update test_batch_limits.rs if the constant assertion changes.
  7. Commit with message: perf: update MAX_BATCH_SIZE to <N> based on <sdk-version> benchmarks.

FilePurpose
contracts/program-escrow/src/lib.rsMAX_BATCH_SIZE constant + pre-flight check
contracts/program-escrow/src/test_batch_limits.rsUnit tests for the constant and typed error
contracts/program-escrow/src/test_batch_operations.rsIntegration tests including oversized batch rejection
docs/gas-optimization/batch-payout-benchmarks.mdBenchmark collection process
benchmarks/program-escrow/thresholds.jsonCI gate thresholds