Skip to content

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():

rust
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

PropertyValue
Default500,000
ScopeAll for, while, and for..in loops share a single budget per bar
ResetCounter resets at the start of each bar execution
DisableSet 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.

rust
// 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

PropertyValue
Default3
ScopeMaximum nesting depth of request.security() calls
DisableSet 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().

rust
// 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

PropertyValue
Default40
ScopeMaximum number of unique request stream keys across all request.security() call sites
DisableSet 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.

rust
// Tighten the limit for resource-constrained environments
let limits = ExecutionLimits::default()
    .with_max_security_calls(10);

max_bars_back

PropertyValue
Default1000
EffectHard upper limit on historical bars retained in memory for series variables and candlestick data
On exceedOldest bars are discarded; history references like close[N] where N >= max_bars_back return na
rust
// 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:

rust
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_depth to 1 or 0 if request.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::timeout as a secondary safeguard, since the loop limit only guards against excessive iteration counts.
rust
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

Released under the MIT License.