vike
Scripting

Limits

The resource ceilings build_engine sets on every strategy script, what trips each one, and what the strategy does when one trips.

A plain rhai::Engine::new() leaves most of these ceilings OPEN: no operation cap, and no size bound on a string, array or map. Of the five below, call depth is the one rhai already defaults — MAX_CALL_STACK_DEPTH, 64 in a release build and 8 in a debug one — so set_max_call_levels(64) pins that release default rather than adding a missing ceiling, and in a debug build it raises it from 8 to 64. crates/vike-script/src/engine.rs's build_engine opts every strategy script into all five so that a runaway or malicious script cannot hang the strategy loop, and crates/vike-script/src/indicator.rs's indicator_engine sets the same five so that one runaway script is bounded the same way whichever seam loaded it.

The five ceilings

ceilingvaluerhai setterwhat it bounds
operations2,000,000set_max_operationsoperations per hook call, "to avoid consuming too much resources"
call depth64set_max_call_levelslevels of function calls, "to avoid infinite recursion and stack overflows"
string size8 KiB (8 × 1024 bytes)set_max_string_sizethe length, in bytes, of a string
array size4096set_max_array_sizethe length of an array
map size4096set_max_map_sizethe size of an object map

The last column quotes the setters' own doc comments (rhai 1.25.1, src/api/limits.rs). Each value is a literal in build_engine; no settings file raises or lowers them.

Per hook call, not per lifetime

build_engine's doc: the limits are PER-HOOK-INVOCATION — each call_fn gets a fresh operation counter — not cumulative across the strategy's lifetime. A script gets two million operations for every on_bar, however many bars it has already seen. build_engine itself runs once per mounted strategy, never per bar.

What a script sees

Rhai reports a tripped ceiling as a runtime error, never as a value: EvalAltResult::ErrorTooManyOperations (rendered Too many operations), ErrorStackOverflow (Stack overflow) or ErrorDataTooLarge (<type> too large). Nothing distinguishes one from any other runtime error on the way out, and the strategy's fail-safe treats them all alike.

That fail-safe is crates/vike-script/src/strategy.rs's RhaiStrategy::run_hook, and the module header states the contract: a hook that errors emits zero orders that bar — intents recorded before the error are discarded — and the strategy keeps running. The error is logged at warn as rhai hook errored; emitting no orders this bar, with the hook name and the script error. After 10 CONSECUTIVE errors the strategy self-disables: every later hook call is a no-op, and rhai strategy disabled after 10 consecutive errors is logged once at error. A successful hook call resets the counter.

The pinned evidence is in crates/vike-script/tests/binding.rs. infinite_loop_trips_op_cap_no_hang compiles fn on_bar() { let i = 0; while true { i += 1; } buy(1.0); }, calls it once, and asserts that the call returned and submitted nothing. self_disables_after_ten_consecutive_errors_then_stays_inert errors on the first ten bars and would buy(1.0) on the eleventh if the body ever ran again; it does not.

Ten errors of any kind, not ten limit trips

The counter does not care why a hook failed, and three other faults count the same way, each firing on every bar: a wrong-typed indicator argument (sma("20") raises bad_arg's error rather than returning NaN — deliberately, because NaN is the warm-up value and a strategy reading a permanent warm-up does nothing forever without saying why); a call to an indicator name the bridge never bound (a Rhai function-not-found, which RHAI_INDICATORS's doc says self-disables the strategy after the cap); and a user indicator that threw, raised at the call site. A strategy with any of these in on_bar is dead after ten bars, and the log is where it says so.

Compile time is bounded too

The top level of a script — everything outside a fn — runs EXACTLY ONCE, inside RhaiStrategy::compile, on the same engine with the same limits. A ceiling tripped there is a ScriptError and the strategy does not mount; discover_params walks the same path when it lists a script's param() knobs. A top-level loop that would exhaust the counter therefore fails at compile, where an author sees it, rather than on the first bar.

A user indicator faults differently

indicator_engine sets the same five numbers, but an indicator's on_bar has no error channel — vike_indicators::Indicator::on_bar returns a Vec<f64>. A script that trips a limit, or throws for any other reason, reads as NaN on every one of its lines for that bar, and the fault is RECORDED rather than dropped: RhaiIndicator::fault holds the message, and a later successful bar (or reset) clears it. The strategy-side bridge, engine.rs's register_user_indicators, reads that fault after feeding the bar and raises it as a Rhai error at the accessor the author called — so a strategy sees an error, not a warm-up NaN, and the ten-error counter applies. A consumer reading through the Indicator trait directly — the chart plots the same compiled prototype — gets the NaN and nothing else.

The study tier is not this

Rhai studies run on a different engine with different ceilings, and the difference is the unit of work rather than a relaxation of standards: crates/vike-studio-core/src/rhai_study/bind.rs's MAX_OPERATIONS is 1,000,000,000 because a study is called ONCE for a whole window, and two million would refuse a plain loop over a hundred thousand bars; MAX_CALL_LEVELS stays 64, because a runaway recursion is a stack overflow whichever tier it is in; and the size limits are deliberately unset, because the big arrays a study holds are the ones the host handed it out of the store. Rhai studies carries that engine.

What is not a ceiling

Two closures on the same engine are refusals rather than numbers, and they belong to The engine: import is refused outright (DummyModuleResolver), and print/debug go to the tracing facade under the vike_script target instead of the host's stdout. The second has no limit at all — a print inside on_bar fires once per bar, at INFO, into the file log layer, which defaults to trace, so a chatty script over a long backtest writes into <project>/settings/state/logs; VIKE_LOG_FILE_LEVEL is the knob, and redirect_script_output's doc carries the argument.

On this page