Security & Execution Limits
When embedding Navi in a host application, user-supplied Navi scripts execute inside the VM. The VM enforces configurable limits to protect the host process from runaway scripts.
Why Limits Matter
Navi scripts can contain loops (for, while, for..in) that may iterate arbitrarily many times depending on user input or data. Without limits, a malicious or buggy script could hang the host process indefinitely. Navi's execution limits are checked per bar so the overhead is predictable.
Configuring Limits
Pass an ExecutionLimits value to InstanceBuilder::with_execution_limits before calling build():
use navi_vm::{ExecutionLimits, Instance, TimeFrame};
let limits = ExecutionLimits::default()
.with_max_loop_iterations_per_bar(1_000_000);
let mut instance = Instance::builder(provider, source, TimeFrame::days(1), "NASDAQ:AAPL")
.with_execution_limits(limits)
.build().await?;When the limit is exceeded the VM raises a runtime error (Error::Exception) that you handle the same way as any other runtime exception.
Available Limits
max_loop_iterations_per_bar
| Property | Value |
|---|---|
| Default | 500,000 |
| Scope | All for, while, and for..in loops share a single budget per bar |
| Reset | Counter resets at the start of each bar execution |
| Disable | Set to u64::MAX to remove the limit |
All loop types count against the same budget within a single bar. For example, a script with two nested for loops each iterating 1,000 times consumes 1,000,000 iterations — exceeding the default budget of 500,000.
// Raise the limit for compute-heavy scripts
let limits = ExecutionLimits::default()
.with_max_loop_iterations_per_bar(2_000_000);
// Disable the limit entirely (not recommended for untrusted scripts)
let limits = ExecutionLimits::default()
.with_max_loop_iterations_per_bar(u64::MAX);max_security_depth
| Property | Value |
|---|---|
| Default | 3 |
| Scope | Maximum nesting depth of request.security() calls |
| Disable | Set to 0 to prohibit request.security() entirely |
Controls how many levels deep request.security() calls may nest. A depth of 1 means a security expression may not itself call request.security().
// Allow only one level of request.security() (no nesting)
let limits = ExecutionLimits::default()
.with_max_security_depth(1);
// Prohibit request.security() entirely
let limits = ExecutionLimits::default()
.with_max_security_depth(0);max_security_calls
| Property | Value |
|---|---|
| Default | 40 |
| Scope | Maximum number of unique request stream keys across all request.security() call sites |
| Disable | Set to 0 to prohibit request.security() entirely |
Each distinct (symbol, timeframe, calc_bars_count) combination creates a separate data stream. Multiple call sites that share the same full key count as one.
// Tighten the limit for resource-constrained environments
let limits = ExecutionLimits::default()
.with_max_security_calls(10);max_bars_back
| Property | Value |
|---|---|
| Default | 1000 |
| Effect | Hard upper limit on historical bars retained in memory for series variables and candlestick data |
| On exceed | Oldest bars are discarded; history references like close[N] where N >= max_bars_back return na |
// Reduce memory usage for long-running scripts
let limits = ExecutionLimits::default()
.with_max_bars_back(500);
// Keep more history for scripts that reference deep history
let limits = ExecutionLimits::default()
.with_max_bars_back(5000);Scripts can adjust the history buffer within this hard limit using:
indicator(max_bars_back=N)/strategy(max_bars_back=N)— sets the script-level default for all variables.max_bars_back(variable, N)— sets the buffer for a specific variable or built-in series (close,open, etc.).
Both are capped by ExecutionLimits.max_bars_back and cannot exceed it.
Handling Limit Violations
A limit violation surfaces as Error::Exception, the same type used for runtime.error() and other runtime errors:
match instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)).await {
Err(navi_vm::Error::Exception(e)) => {
eprintln!("Script error: {}", e.display());
}
_ => {}
}See Error Handling for full details on handling runtime errors.
Recommendations
- Untrusted scripts: keep the default limits or lower them. Consider setting
max_security_depthto1or0ifrequest.security()is not needed. - Known scripts: raise limits only if you have confirmed the script requires more resources.
- Server-side execution: wrap execution calls with
tokio::timeoutas a secondary safeguard, since the loop limit only guards against excessive iteration counts.
use std::time::Duration;
use tokio::time::timeout;
let result = timeout(
Duration::from_secs(30),
instance.run_to_end("NASDAQ:AAPL", TimeFrame::days(1)),
).await;Next Steps
- Snapshots — saving and restoring VM state across restarts
- JIT Compilation — enabling ahead-of-time compilation for faster execution