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
- Estimate —
packet.EstimateTransaction(envelope)→ read the profile andsuggested. - Declare — set the budget at the layer that owns the decision, starting from
suggestedand rounding to headroom you’re comfortable with. - Lint — drop a
.headroomsidecar 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.