Overview
Every transaction runs under a resource budget: a set of per-dimension caps on how much work and memory it may use. A transaction aborts the instant any one dimension is exceeded. The budget exists so a hostile or buggy caller cannot drive unbounded recursion, allocation, or computation on a node processing the transaction.
Because every node that processes a transaction must reach the same accept/abort decision, every limit is a fixed count, identical on every target. No limit is derived from the amount of memory, the pointer width, or the wall-clock speed of a particular machine — a machine-dependent limit would let two nodes disagree on the same transaction.
The metered dimensions
A budget has nine dimensions. Each has a single canonical name — the same name is used everywhere a
limit is written (a transaction limits(...) clause, a packet governance update, a signed envelope,
or the SDK).
| Name | Counts | Kind |
|---|---|---|
fuel_ops |
weighted operations (the length-weighted 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 |
string_bytes |
longest string/blob byte length | structural peak |
packet_elems |
runtime elements in one packet | structural peak |
render_bytes |
bytes produced by one value render (_str, error text, …) |
structural peak |
A dimension that is not set anywhere inherits the global default. The set of names is closed: any other name is rejected wherever a limit is written.
How a limit is composed
The effective cap for a dimension is the tightest value produced by four layers. Each layer is owned by a different party:
| Layer | Who sets it | Direction |
|---|---|---|
| Global default / ceiling | the protocol | fixed; every other layer resolves within it |
| Packet governance | the packet author (a governance update) | raise or tighten the default, clamped to the hard bound |
| Source clause | the contract author (a limits(...) clause) |
tighten only |
| Signed envelope | the submitter (a per-call __limits) |
tighten only |
Governance is the only layer that may raise a dimension above the default; it composes by replacement (the governance value wins in either direction) and is then clamped to the dimension’s hard bound. The two untrusted inner layers — the source clause and the signed envelope — are tighten-only: they compose by minimum and can never loosen a dimension above the value the outer layers already fixed. Adding an inner layer can only ever lower the effective cap.
Equivalently, for each dimension:
effective = min( clause, envelope, min( governance-over-default, hard_bound ) )
where an unset layer contributes nothing (it defers to the others).
What the budget counts
The budget is metered over the whole transaction, not just the code you wrote. When a transaction is invoked, the meters are armed before the signed envelope is decoded and unpacked, and the consumed tally runs continuously from that point through the transaction body. So the work the system does to decode and validate the envelope is charged against the same budget as the body.
This is deliberate. The clause and signed-envelope layers tighten the limit that applies once the body runs, but they do not reset the tally — the envelope-processing cost is already on the meter when the body’s tighter limit takes effect. A declared limit therefore has to accommodate the whole transaction, envelope processing included. That is a small, fixed per-transaction overhead, so in practice it only matters for a budget set deliberately close to the transaction’s true cost.
Hard bounds
Five dimensions carry a hard bound — a format cap that no layer, including governance, can push past:
| Dimension | Default | Hard bound |
|---|---|---|
elem_count |
16,777,216 (2^24) | 16,777,216 (2^24) |
string_bytes |
4,294,967,296 (2^32) | 4,294,967,296 (2^32) |
constructed_elems |
1,048,576 (2^20) | 1,048,576 (2^20) |
packet_elems |
16,777,216 (2^24) | 16,777,216 (2^24) |
render_bytes |
16,777,216 (16 MiB) | 4,294,967,296 (2^32) |
For the first four the default is the hard bound, so governance cannot raise them. render_bytes
is the exception: its default (16 MiB) sits below its hard bound (4 GiB), so governance may raise
the effective value toward the ceiling — while the clause and envelope layers can still only tighten.
A governance value outside the valid range [1, ceiling] for a dimension is rejected when it is
set, with an out-of-range error.
The remaining four dimensions — fuel_ops, eval_calls, call_depth, and value_height — have no
fixed hard bound and may be raised freely by governance. Raising fuel_ops or eval_calls only
spends more deterministic fuel. Raising call_depth or value_height past a machine’s stack
requires reconfiguring that machine’s process stack (an out-of-band operational step), so there is
no fixed count that must hold.
These are format bounds on counts, not memory bounds. A host with less memory than a cap implies will run out of memory before the cap is reached.
Named tiers
Three vetted bundles can be selected by name in a limits(...) clause instead of writing each
dimension out:
| Tier | fuel_ops |
call_depth |
constructed_elems |
|---|---|---|---|
$limits::small |
10,000 | 64 | 4,000 |
$limits::standard |
100,000 | 256 | 40,000 |
$limits::heavy |
500,000 | 1,024 | 200,000 |
A tier resolves at compile time to a limit set; an explicit dimension written after a tier overrides that tier entry. See the limits clause for the syntax.
packet_elems is enforced at seal
packet_elems bounds the number of runtime elements in one packet. Unlike the other dimensions it
is checked when the packet is sealed, and it honors only the global default and packet
governance — a transaction source clause and a signed envelope do not apply to it.
render_bytes and how a render responds to it
render_bytes bounds the number of bytes produced by a single value render — _str, _code_str,
JSON rendering, an error/diagnostic string, and the like. The byte count is charged as each byte is
produced, and the render stops the instant the running total exceeds the limit, so a structurally
shared value whose rendered form would expand far beyond its in-memory size never materializes the
blow-up.
What happens on overflow depends on where the render feeds:
- A render whose result becomes part of committed transaction state (a value a consumer keeps) aborts the transaction on overflow. It is never silently truncated — a truncated value would differ across nodes with different budgets and split the network.
- A diagnostic render (a display, log line, or error-text string) instead emits a sentinel marker and stops. Because the truncation point is a fixed cross-target limit, the truncated text is identical everywhere, so it is safe even when that text is itself hashed within the transaction.
render_bytes is a configurable dimension like any other: it can be set in a limits(...) clause, a
governance update, or a signed envelope, and (uniquely) governance may raise it above the 16 MiB
default toward the 4 GiB hard bound.
Where to go next
- The limits clause and packet governance — the syntax for declaring limits on a transaction and for a packet to set its own defaults.
- Limits reference — the per-dimension table: default, hard bound, and what exceeding each dimension does.
- Fuel cost model — how
fuel_opsis charged per operation. - Choosing your limits — the estimate → declare → lint workflow for picking a budget you can trust.