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
| ceiling | value | rhai setter | what it bounds |
|---|---|---|---|
| operations | 2,000,000 | set_max_operations | operations per hook call, "to avoid consuming too much resources" |
| call depth | 64 | set_max_call_levels | levels of function calls, "to avoid infinite recursion and stack overflows" |
| string size | 8 KiB (8 × 1024 bytes) | set_max_string_size | the length, in bytes, of a string |
| array size | 4096 | set_max_array_size | the length of an array |
| map size | 4096 | set_max_map_size | the 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.
Migration
The one-time move of the Studio's studio_strategies.json rows into user_data/strategies/ — what each row becomes, what is reported instead of written, and why the JSON is never touched.
Cheatsheet
Every host function a strategy script can call, every hook it can define, and the indicator call forms — one scannable page beside the full ones.