Limits reference
Every limit in LimitsProfile::InteractiveV1, its default value, and what it bounds.
Limits carries 27 numeric bounds plus one optional deadline. The values below are the
frozen defaults of LimitsProfile::InteractiveV1, which is what
Limits::default() returns and what every no-argument convenience in the crate runs
under. Those numbers never change: a future retuning ships as a new profile variant with a new wire
name rather than as an edit to this one. The single source of truth is the
limit_fields! block in
src/limits.rs.
Every limit has a getter named exactly after the field (limits.max_input_bytes()) and a
chainable builder (.with_max_input_bytes(n)). Fields are private, so there is no struct
literal and adding a limit is never a breaking change. Every limit accepts 0 as a
defined fail-closed policy. For what all of this is for, read
Limits, budgets and diagnostics first.
Reading the tables#
-
Reported as is the
resourcestring that appears in aresource_limit_exceededdiagnostic's{resource, limit, observed}triple. It is usually the field name minus itsmax_prefix, but not always: several limits are charged under more than one resource name, andextension_byteshas no field of its own. Match the resource string as data; do not assume amax_<resource>field exists. -
Surface names the Cargo feature under which the limit is enforced.
base means it applies with
default-features = false; compiler and host-value mean the code that charges it is behind that feature. See Install and feature surfaces.
Input and source bytes#
| Limit | Default | Reported as | What it bounds | Surface |
|---|---|---|---|---|
max_input_bytes |
4 MiB (4 194 304) | input_bytes |
Bytes accepted by a bounded parse entry point before the document is decoded. Also gates the slice passed to artifact_id_with_limits. |
base |
max_source_bytes |
1 MiB (1 048 576) | source_bytes |
A single resolved DID source. | compiler |
max_bundle_bytes |
8 MiB (8 388 608) | bundle_bytes |
Aggregate bytes across every source in a resolved bundle. | compiler |
HostValue::from_json_with_limits and from_json_with_context gate on
max_value_bytes, not max_input_bytes, and report
HostValueJsonError::Limit, which carries no resource name. Lowering
max_input_bytes alone does not bound host value decoding — lower
max_value_bytes too.
Bundle and import shape#
| Limit | Default | Reported as | What it bounds | Surface |
|---|---|---|---|---|
max_sources |
256 | sources, source_actors |
Number of sources in a resolved bundle, and the actor entries a provenance sidecar records. | compiler |
max_source_id_bytes |
1024 | source_id_bytes |
A single logical source ID (its name or path), individually — on both the resolver and the embedded-sidecar paths. Without it, one entry could carry a megabyte-long path under the cumulative max_string_bytes alone. |
compiler |
max_import_depth |
64 | import_depth |
Import chain depth during source resolution. | compiler |
max_import_edges |
1024 | import_edges |
Import edges across a resolved bundle. | compiler |
Graph size and depth#
| Limit | Default | Reported as | What it bounds | Surface |
|---|---|---|---|---|
max_source_nesting |
256 | source_nesting |
Lexical nesting: open delimiters plus a run of opt/vec constructors, counted over a token scan before the recursive upstream parser is invoked. |
compiler |
max_type_depth |
256 | type_depth |
Semantic type nesting lowered from a checked Candid program. | compiler |
max_type_nodes |
100 000 | type_nodes |
Type nodes in a Contract arena. | base |
max_graph_edges |
1 000 000 | graph_edges |
Edges in a Contract type graph. | base |
max_declarations |
100 000 | declarations, source_declarations |
Named declarations in a Contract, and declaration entries in a provenance sidecar. | base · compiler |
max_fields |
500 000 | fields, source_field_labels |
Aggregate record and variant fields across a Contract, and field-label entries in a sidecar. Not what bounds a single wide record at validation time — see below. | base · compiler |
max_methods |
100 000 | methods, source_methods |
Aggregate service methods across a Contract, and method entries in a sidecar. | base · compiler |
max_function_values |
500 000 | function_values, source_function_arguments |
Aggregate function arguments and results across a Contract, and argument entries in a sidecar. | base · compiler |
max_string_bytes |
1 MiB (1 048 576) | string_bytes, source_string_bytes |
Aggregate string bytes across declaration and method names. The same number bounds a provenance sidecar's own strings — source names, import paths, declaration and method names, and documentation text — reported as source_string_bytes. The sidecar's documentation-entry count is checked against that same resource first, as a lower bound over records, before the byte pass walks every entry. |
base · compiler |
max_producer_bytes |
4096 | producer_bytes |
Aggregate bytes across the four ProducerInfo strings. Producer metadata is untrusted, caller-supplied provenance and never influences a semantic identity hash; this bounds what it may contribute to a validated Contract. |
base |
Host value size#
| Limit | Default | Reported as | What it bounds | Surface |
|---|---|---|---|---|
max_value_nesting |
64 | value_nesting |
Lexical JSON container nesting ({ and [), checked by a constant-stack pre-scan before the recursive serde_json decoder runs. |
host-value |
max_value_depth |
256 | value_depth |
Semantic host value nesting, after decoding. Also bounds construction through HostValue::opt, vector, record and variant. |
host-value |
max_value_elements |
1 000 000 | value_elements |
Aggregate host value elements per document. The binding limit for wide vectors and for element counts accumulated across a value tree. | host-value |
max_value_bytes |
16 MiB (16 777 216) | value_bytes, extension_bytes |
Aggregate host value text and blob bytes per document, and the byte gate for host value JSON decoding. The same number bounds the extensions map of a ContractEnvelope, reported as extension_bytes. |
host-value · base |
Work counters#
A work unit is an abstract cost counter for operations whose expense is not a simple byte or item count. The five counters are deliberately not pooled, so one phase of a compilation cannot starve the next on a shared total.
| Limit | Default | Reported as | What it charges | Surface |
|---|---|---|---|---|
max_canonicalization_work |
10 000 000 | canonicalization_work |
Graph nodes and edges visited, signature and string bytes produced, comparison bounds for sorted collections, reindexing and rewriting, canonical bytes serialized and hashed — and the length of any JSON the crate renders. Host value record and variant validation charge the same counter, one unit per field or tag comparison. | base · host-value |
max_provenance_work |
10 000 000 | provenance_work |
Building and querying the field-ID and method-name indexes used to resolve every provenance target in a source sidecar, plus every membership test. | compiler |
max_source_identity_work |
400 000 000 | source_identity_work |
Serializing and hashing the source-bundle identity (candid-core:source-bundle:v1). One unit per serialized payload byte in a counting pass, then two more per byte to materialize and hash the canonical bytes. |
compiler |
max_artifact_identity_work |
10 000 000 | artifact_identity_work |
Hashing a detached artifact, at exactly bytes.len() + domain.len() + 1 units. There is no length field and no second kind label in the preimage, so that constant is the whole overhead. |
base |
max_type_preflight_work |
10 000 000 | type_preflight_work |
The two iterative walks that guard max_type_depth: one unit per visited syntax node or expansion state, plus one per recursive name tracked on the state's path. |
compiler |
Two of those defaults are derived rather than guessed.
max_source_identity_work at 400 000 000 covers the worst case the default byte limits
admit: JSON string escaping expands one byte to at most six, so a compile pass costs about 213
million units and the two passes of a presented-sidecar validation about 341 million together.
max_artifact_identity_work at 10 000 000 covers a 4 MiB artifact, the largest the
default byte gate admits, under the longest frozen domain tag
candid-core:artifact:contract-envelope-json:v1 at 46 bytes: 4 194 351 units.
Diagnostics#
| Limit | Default | Reported as | What it bounds | Surface |
|---|---|---|---|---|
max_diagnostics |
100 | diagnostics |
Retained violations per Contract structure validation failure — the collector behind ContractValidationError. When more are observed than fit, the last retained slot is replaced by a resource_limit_exceeded sentinel at path $ carrying the true observed count, updated in place on every later observation. A cap of 0 retains exactly that one sentinel, so an invalid input never yields an empty error collection. Compile diagnostics come from the upstream checker and are not capped by this limit. |
base |
The deadline#
deadline_unix_ms is the one field that is not a usize count. It is an
Option<u64> Unix timestamp in milliseconds, defaulting to None, set
with with_deadline_unix_ms(Some(ms)) and cleared with None. Any value at
or before the current time, Some(0) included, makes every bounded operation fail
closed with operation_deadline_exceeded before doing work. It is configured in
wall-clock time and enforced against a monotonic clock snapshotted when the operation begins.
Limits::deadline_exceeded() reports whether it has elapsed.
The portable serialized configuration#
Limits never serializes as a bare field map. It serializes as a versioned document
that names its frozen baseline and carries only the fields you actually changed:
{
"version": 1,
"profile": "interactive_v1",
"overrides": {
"max_input_bytes": 512,
"max_diagnostics": 0,
"deadline_unix_ms": 2000000000000
}
}
That document is the exact serialization of
Limits::default().with_max_input_bytes(512).with_max_diagnostics(0).with_deadline_unix_ms(Some(2_000_000_000_000)),
and it round-trips back to the same value. Limits::default() itself serializes to
exactly:
{"version":1,"profile":"interactive_v1","overrides":{}}The rules the format enforces:
-
versionpins the schema.LIMITS_CONFIG_VERSIONis1. A different version is rejected withunsupported_limits_versionat$.version. -
profilenames the frozen baseline."interactive_v1"is the only released wire name. An unknown one is rejected withunsupported_limits_profileat$.profile. -
Override values are fixed-width
u64. They narrow to the host'susizethrough one checked conversion at the configuration boundary: never truncated, never wrapped. A value the platform cannot represent is rejected withlimit_override_unrepresentableat$.overrides.<field>. Because the profile carries the defaults by name, one document configures identical policy on a 32-bit and a 64-bit host. -
Only overrides that differ from the profile are serialized. An override set
explicitly back to its profile value normalizes away, so omission and an equal explicit value are
the same policy forever. A missing
overridesobject means no overrides. -
An explicit
nullis rejected. Absence is the only spelling of "use the profile value".{"overrides": {"max_input_bytes": null}}is a decode error, not a second way to say nothing. -
Unknown keys are rejected, both at the top level and inside
overrides.
RuntimeContext serializes as {"limits": …} and nothing else. A document
that also carries a "cancellation" key is rejected; the token is host-local
bookkeeping and never travels.
Decoding a configuration with a structured error#
Deserializing straight into Limits wraps a rejection in a serde error string. Decode
to LimitsConfig first and convert with TryFrom when you want the
code, path and message programmatically.
use candid_core::{Limits, LimitsConfig};
let config: LimitsConfig = serde_json::from_str(document)?;
let limits = Limits::try_from(config).map_err(|error| {
// code() == "unsupported_limits_version", path() == "$.version".
// Display renders as:
// unsupported_limits_version at $.version: unsupported limits config
// version 2; this build supports version 1
eprintln!("{}: {}", error.code(), error.path());
error
})?;
New limits are added as additive override keys without bumping
LIMITS_CONFIG_VERSION, because Limits fields are private and adding one
breaks nothing. A document that leaves a new limit at its profile value therefore carries no key
for it and is compatible in both directions. A document that does carry, say,
max_type_preflight_work is readable by this build and newer ones and rejected by a
build predating the key. That is the intended failure: silently ignoring an unknown limit
override would apply a policy the document did not ask for.
Interactions worth knowing before you tune#
max_canonicalization_work binds a record at 2 581 fields#
Host value record validation is deliberately allocation-free: instead of building a field-ID index
it scans pairwise and charges one canonicalization_work unit per comparison. Three
such scans run per record (duplicate detection, field-set agreement, per-field lookup), so cost is
roughly 1.5n² units for an n-field record. At the default 10 000 000 that
puts the ceiling at 2 581 fields: a 2 582-field record fails closed naming
canonicalization_work, not fields and not
value_elements.
That is three orders of magnitude below the 500 000 fields max_fields permits and the
1 000 000 elements max_value_elements permits. Neither of those is the binding limit
for a wide record. Raise the work counter to validate wider ones, and note the cost grows
quadratically. The counter is per operation and shared with Contract canonicalization, so several
moderately wide records in one value tree accumulate against the same budget.
Raising max_value_nesting above 128 does nothing#
serde_json applies a fixed 128-frame recursion ceiling that this crate deliberately
does not disable. The default of 64 sits below it so the crate's own constant-stack pre-scan is
always the check that fires, giving you a value_nesting triple instead of an opaque
serde string. Set the limit above 128 and documents nested deeper are rejected by the decoder as
malformed instead. A host on an ordinary 8 MiB stack can raise it to 128 without approaching either
bound.
Raise max_input_bytes and max_artifact_identity_work together#
Artifact hashing costs exactly bytes.len() + domain.len() + 1 units. Raise the byte
gate above the artifact work limit without raising the work limit and over-sized artifacts start
failing on artifact_identity_work instead of input_bytes, a failure that
names the wrong knob. The two defaults are consistent for every declared
ArtifactKind, and a test asserts that.
max_type_preflight_work is a memory knob as well as a time knob#
The type-depth guard walks deduplicate expansion states into a memo retained for the duration of
the walk, where the tree walks they replaced held only a stack. So the counter bounds heap as well
as work, at a measured rate of about 19 bytes of peak live heap per unit. The 10 000 000 default
therefore authorizes roughly 190 MB before the counter refuses, which is more than a small 32-bit
wasm32 heap can serve; a host whose allocator gives out before the counter does gets
an allocation abort instead of the clean structured refusal. A browser or other constrained host
should lower this to around 1 000 000 (about 19 MB), which still clears every realistic contract
by a wide margin. Rejecting costs no memory, so no input can force an abort by being
larger than the configured bound, only by being accepted under a bound the host cannot
honour.
The candid-core binary has no flags for limits#
candid-core compile and candid-core validate bound their reads under
Limits::default(), and there is no option to change that. Pass a custom policy through
the library APIs instead. compile bounds each source by max_source_bytes
and the bundle by max_bundle_bytes, failing in the diagnostics shape;
validate bounds the document by max_input_bytes, failing in the
violations shape.
Why every entry point is bounded, and what the caller observes when one trips.
Rust crate Working with limitsProfiles, builders, deadlines and cancellation in practice.
Reference JSON document formatsThe Contract, envelope, compilation, host value and limits documents.