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 resource string that appears in a resource_limit_exceeded diagnostic's {resource, limit, observed} triple. It is usually the field name minus its max_ prefix, but not always: several limits are charged under more than one resource name, and extension_bytes has no field of its own. Match the resource string as data; do not assume a max_<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#

LimitDefaultReported asWhat it boundsSurface
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
Host value JSON is the one exception to the byte gate

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#

LimitDefaultReported asWhat it boundsSurface
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#

LimitDefaultReported asWhat it boundsSurface
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#

LimitDefaultReported asWhat it boundsSurface
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.

LimitDefaultReported asWhat it chargesSurface
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#

LimitDefaultReported asWhat it boundsSurface
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:

json
{
  "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:

json
{"version":1,"profile":"interactive_v1","overrides":{}}

The rules the format enforces:

  • version pins the schema. LIMITS_CONFIG_VERSION is 1. A different version is rejected with unsupported_limits_version at $.version.
  • profile names the frozen baseline. "interactive_v1" is the only released wire name. An unknown one is rejected with unsupported_limits_profile at $.profile.
  • Override values are fixed-width u64. They narrow to the host's usize through one checked conversion at the configuration boundary: never truncated, never wrapped. A value the platform cannot represent is rejected with limit_override_unrepresentable at $.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 overrides object means no overrides.
  • An explicit null is 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.

rust
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
})?;
A newer limit key is rejected by an older build, on purpose

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.