Skip to content

Security & Execution Limits

When embedding OpenPine in a host application, user-supplied Pine scripts execute inside the VM. The VM enforces configurable limits to protect the host process from runaway scripts.

Why Limits Matter

Pine 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. OpenPine'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 openpine_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(). The default of 3 matches TradingView's behaviour.

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 (symbol, timeframe) pairs across all request.security() call sites
DisableSet to 0 to prohibit request.security() entirely

Each distinct (symbol, timeframe) combination creates a separate data stream. Multiple call sites that share the same pair count as one. The default of 40 matches TradingView's behaviour.

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

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(openpine_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: consider running each Instance in a dedicated thread with an OS-level timeout as a secondary safeguard, since the loop limit only guards against excessive iteration counts, not other sources of long-running computation.

Next Steps

Released under the MIT License.