Overview

Every transaction runs under a resource budget. If the budget is too low the transaction aborts; too high and a hostile or buggy caller can burn far more work than you intended before the ceiling catches it. This is the workflow for picking a budget you can trust: estimate what a transaction really costs, declare a budget with headroom, then lint so a too-tight declaration is caught in test rather than in production.

You never have to guess. The engine will tell you the exact cost, suggest a budget, and — when a budget is hit — tell you which layer bound it.

The dimensions you tune

A budget is a set of per-dimension caps; a transaction aborts the instant any one is exceeded. All nine dimensions are listed in the limits reference; the six a transaction author usually tunes are:

Name Counts Kind
fuel_ops weighted operations (the fuel cost model) consumed total
eval_calls evaluator node evaluations consumed total
constructed_elems evaluator-constructed runtime values consumed total
call_depth deepest lambda-call nesting reached structural peak
value_height deepest value nesting constructed structural peak
elem_count most elements in one container structural peak

An unset dimension defaults to the global ceiling. In a transaction clause or envelope you only ever set a dimension below the ceiling — those layers are tighten-only.

Step 1 — Estimate

Dry-run the transaction to measure its exact consumption. The estimator forks the packet, runs the transaction in observe mode (it counts but never trips), and discards the fork. It touches no committed state and changes no hash — the packet is byte-identical afterward.

const profile = packet.EstimateTransaction(envelope);
// {
//   ops, evalCalls, constructed,                     // the consumed totals
//   peakCallDepth, peakValueHeight, peakElemCount,   // the structural peaks
//   suggested: { fuel_ops, eval_calls, ... }         // an advisory budget
// }

Two things come back:

  • The profile — the exact figure any node will compute for this (program, arguments, state). Observe-mode counts equal enforce-mode counts exactly; a dry run is not an approximation.
  • suggested — each measured value plus 50% headroom, clamped to the global ceiling. It is advisory: a hint to inform your next submission, never armed or committed. Treat it as a starting point, not a rule.

Estimation is only available in test mode. It cannot run against a committed transaction — the observe-mode meter is barred outside test mode by a hard guard, so an estimate can never be mistaken for a real submission.

Step 2 — Declare a budget

A transaction’s effective budget is the tightest value at each dimension across every layer that applies (min-composition). The budget is charged over the whole transaction — including the work to decode the signed envelope, not just your body code — so leave headroom for that fixed overhead when a budget sits close to the true cost. Pick the layer that matches who owns the decision:

Layer Who sets it How
Global defaults the protocol fixed defaults; five dimensions have a hard bound governance cannot exceed (and only render_bytes, whose default is below its bound, can be raised by governance), the other four have no fixed bound
Packet defaults the packet author a governance update (_set_limits) — may raise a dimension above the global default or tighten it, up to the hard bound where one exists
Source clause the contract author a limits(...) clause on the transaction (tighten-only)
Envelope budget the submitter a per-call __limits on the signed message — from the SDK, the configurator’s --limits flag (a {name: count} JSON object) (tighten-only)

Session client clamps are a separate, local-only knob for the two client-side construction dimensions:

Adapt.setSessionLimits({ decode_depth: 128, value_height: 512 }); // tighten-only, this process only

These refuse oversized values as you build them on the client; they are local to that process and never change the result a processing node computes.

Because composition is min-only, adding a layer can only tighten. When a transaction does hit a budget, the exhaustion message names the meter and the budget it exceeded. On the main metering paths — the operation reduction tick, evaluator calls, and call depth — it also names which layer bound it:

Transaction operation budget exceeded: 20000 weighted operations consumed (bound by envelope)

(bound by global | packet | clause | envelope) tells you exactly which knob to raise. In test mode the message also carries the MUFL call stack at the point of exhaustion, so you can see where the budget blew.

Step 3 — Lint the headroom

A declared budget wants headroom over the observed maximum — enough that ordinary variation in inputs doesn’t trip it, without being so loose it defeats the point. The functional test runner checks this for you.

Add a .headroom sidecar next to the test file, one dim=declared line per dimension you care about:

# my_transaction.mu=jrun+sr.headroom
fuel_ops=40000
eval_calls=8000

Then run the suite with the lint on:

./test.sh --headroom-lint        # default: require >= 2x headroom
./test.sh --headroom-lint=3      # require >= 3x

For each opted-in test the runner records the observed maximum per dimension and fails the test when a declared limit has less than N x headroom over it (declared >= N * observed passes; just under fails). A dimension the run never exercised is skipped — there’s no observation to bound it. This catches a declaration that has quietly drifted too tight before it reaches production.

The loop, end to end

  1. Estimatepacket.EstimateTransaction(envelope) → read the profile and suggested.
  2. Declare — set the budget at the layer that owns the decision, starting from suggested and rounding to headroom you’re comfortable with.
  3. Lint — drop a .headroom sidecar and run ./test.sh --headroom-lint; tighten or loosen until it passes at your chosen factor.

Re-estimate whenever the transaction’s logic or its typical inputs change — the profile moves with them, and the lint will tell you if your old budget no longer has the headroom you signed up for.