Performance

Risor competes with five Go-embeddable runtimes: CEL, Expr, Goja (JavaScript), GopherLua, and OPA/Rego. We benchmark all six across three scenarios, from trivial expressions to multi-step data pipelines.

The short version: runtimes that beat Risor on simple expressions can't handle the harder scenarios at all.

The Scenarios

Simple Expression

A single conditional: given a total and a threshold, return "premium" or "standard".

total > threshold ? "premium" : "standard"

This is the baseline. Every runtime can do this. It tests parse-compile-execute overhead with no data structures or iteration, the kind of check you'd run in a policy gate or feature flag.

All 6 runtimes participate.

Data Processing

Filter 10,000 order records to active orders, sum their amounts, and classify the result. This tests collection operations at scale: filtering, mapping, and aggregation over real data.

let active = orders.filter(o => o.status == "active")
let total = active.map(o => o.amount).reduce(0, (acc, x) => acc + x)
if (total > 100) { "premium" } else { "standard" }

CEL drops out. It has no reduce or sum. You can't aggregate a list in a single CEL expression, so the host would need to loop over results in Go code.

5 runtimes participate: Risor, Expr, Goja, GopherLua, OPA/Rego.

Business Logic

Filter 500 employee records by active status, identify high performers (score >= 4), map each to a bonus amount, sum the bonus pool, and approve or deny based on a budget. This is a realistic multi-step pipeline: two filters, a map, a reduce, and a conditional.

let active = employees.filter(e => e.active)
let top = active.filter(e => e.score >= 4)
let bonuses = top.map(e => e.score * 1000)
let total = bonuses.reduce(0, (acc, b) => acc + b)
if (total > budget) { "over-budget" } else { "approved" }

CEL and Expr both drop out. Neither supports variables or multi-step computation. Anything beyond a single expression requires the host to orchestrate multiple evaluations and pass intermediate results through Go code.

4 runtimes participate: Risor, Goja, GopherLua, OPA/Rego.

Results

All benchmarks run 5,000 iterations after 500 warmup iterations on darwin/arm64 (Apple Silicon). Each runtime-scenario-mode combination runs in a separate subprocess for process isolation.

Warm Eval (Median)

Warm eval is the most representative metric for production use: compile once at startup, then execute per request. These are median latencies.

Simple Expression

RuntimeMedianvs FastestNotes
Expr0.08 µs1×Expression-only
CEL0.08 µs1×Expression-only
Goja (JS)0.60 µs7.5×
OPA/Rego4.90 µs61×
Risor15.20 µs190×
GopherLua29.00 µs363×

Expr and CEL compile to a flat evaluation tree with no VM dispatch. At runtime it's just pointer chasing through a pre-built AST. Goja's V8-style interpreter is also efficient for trivial expressions. Risor's bytecode VM has more overhead per evaluation, but 15 µs is well under the threshold that matters for any real workload.

Data Processing (10,000 records)

RuntimeMedianvs FastestNotes
Expr0.85 ms1×
Goja (JS)2.42 ms2.8×
Risor4.20 ms4.9×
GopherLua7.62 ms8.9×
OPA/Rego11.91 ms14.0×
CEL------Can't express this

With real data, the picture shifts. OPA was 3× faster than Risor on simple expressions, but at scale it's 2.8× slower. OPA's Rego evaluation model re-evaluates rules per-row through its query planner, which compounds with dataset size. GopherLua pays for Lua's imperative loop model and heavy per-value boxing. Risor's functional pipeline (filter/map/reduce) is compiled to bytecode and runs without intermediate Go allocations.

Business Logic (500 records)

RuntimeMedianvs FastestNotes
Goja (JS)120 µs1×
Risor273 µs2.3×
GopherLua611 µs5.1×
OPA/Rego1,380 µs11.5×
CEL------Can't express this
Expr------Can't express this

Two of the six runtimes can't participate at all. Of the four that can, Risor is close to Goja and well ahead of Lua and OPA. At 273 µs you can run this logic thousands of times per second per core.

Memory Footprint

Heap memory per compiled instance, measured via runtime.MemStats.

Simple Expression

RuntimeHeap per Instancevs Smallest
Expr1.9 KB1×
Risor2.2 KB1.2×
Goja (JS)3.3 KB1.7×
CEL23.1 KB12×
OPA/Rego80.0 KB42×
GopherLua179.8 KB95×

Risor's compiled representation is almost as small as Expr, which stores a single expression tree. CEL carries protobuf type metadata. OPA builds a partial evaluation index. GopherLua allocates a full Lua VM state with its standard library.

Business Logic

RuntimeHeap per Instancevs Smallest
Risor7.6 KB1×
OPA/Rego100.4 KB13×
Goja (JS)114.0 KB15×
GopherLua1,889.0 KB249×

If you're running thousands of script instances (multi-tenant rule evaluation, per-request policy checks), 7.6 KB vs 1.9 MB per instance is the difference between fitting in L2 cache and thrashing main memory. GopherLua's nearly 2 MB per instance makes it impractical for high-density workloads.

What This Means

No runtime wins everywhere. The question is which one can handle what you actually need to do.

Expr and CEL are the right choice if your use case is genuinely a single boolean expression: a policy check, a feature flag, a validation rule. They're fast and tiny. But the moment you need variables or multi-step logic, you're back to writing Go.

Goja is fast and JavaScript is familiar. The tradeoff is a larger memory footprint under load, and you inherit JS semantics (prototype chains, type coercion, this binding) in your embedded scripts.

OPA/Rego is built for policy-as-code and works well there. But its declarative evaluation model is slow for general computation, and Rego's syntax is unfamiliar to most developers.

GopherLua has the worst memory profile by a wide margin. At 33 MB for data processing and 1.9 MB for business logic, it's not viable for high-density use cases. The Go implementation is also no longer actively maintained.

Risor is the only runtime that participates in all three scenarios with consistent performance and a 2-8 KB memory footprint. It won't beat a purpose-built expression evaluator at evaluating expressions. But it handles simple conditions, data pipelines, and multi-step business logic in a single sandboxed runtime.


Risor © 2026 Deep Noodle