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
| Runtime | Median | vs Fastest | Notes |
|---|---|---|---|
| Expr | 0.08 µs | 1× | Expression-only |
| CEL | 0.08 µs | 1× | Expression-only |
| Goja (JS) | 0.60 µs | 7.5× | |
| OPA/Rego | 4.90 µs | 61× | |
| Risor | 15.20 µs | 190× | |
| GopherLua | 29.00 µs | 363× |
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)
| Runtime | Median | vs Fastest | Notes |
|---|---|---|---|
| Expr | 0.85 ms | 1× | |
| Goja (JS) | 2.42 ms | 2.8× | |
| Risor | 4.20 ms | 4.9× | |
| GopherLua | 7.62 ms | 8.9× | |
| OPA/Rego | 11.91 ms | 14.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)
| Runtime | Median | vs Fastest | Notes |
|---|---|---|---|
| Goja (JS) | 120 µs | 1× | |
| Risor | 273 µs | 2.3× | |
| GopherLua | 611 µs | 5.1× | |
| OPA/Rego | 1,380 µs | 11.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
| Runtime | Heap per Instance | vs Smallest |
|---|---|---|
| Expr | 1.9 KB | 1× |
| Risor | 2.2 KB | 1.2× |
| Goja (JS) | 3.3 KB | 1.7× |
| CEL | 23.1 KB | 12× |
| OPA/Rego | 80.0 KB | 42× |
| GopherLua | 179.8 KB | 95× |
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
| Runtime | Heap per Instance | vs Smallest |
|---|---|---|
| Risor | 7.6 KB | 1× |
| OPA/Rego | 100.4 KB | 13× |
| Goja (JS) | 114.0 KB | 15× |
| GopherLua | 1,889.0 KB | 249× |
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.