Merged
Size
XL
Change Breakdown
Feature50%
Performance30%
Config10%
Refactor10%
#27573feat(core): Add isolate pooling for VM expression engine

Isolate pooling added to expression engine

Isolate pooling added to expression engine

V8 isolates are now pooled for workflow executions, eliminating the cold-start latency that came with creating a new isolate for each expression.

When workflows evaluate expressions using the VM engine, n8n previously created and destroyed V8 isolates for each evaluation. This cold-start overhead added latency to every expression.

A new isolate pool keeps a set of pre-warmed V8 isolates ready. When a workflow execution begins, it acquires one isolate from the pool and holds it for the duration. On completion, the isolate returns to the pool for background replenishment. The pool size scales with the execution concurrency limit, ensuring a warm isolate is always available when a new execution starts.

Credential resolution outside the execution context uses the same acquire-evaluate-release path. If the pool is exhausted, it falls back to a cold-start bridge rather than failing.

In the @n8n/expression-runtime package, the ExpressionEvaluator now manages bridges through a IsolatePool instance, tracking them by Expression owner using WeakMap. The acquire(caller) and release(caller) methods bind an isolate to an Expression instance for its lifetime. New configuration options control pool behavior: N8N_EXPRESSION_ENGINE selects legacy or VM mode, N8N_EXPRESSION_ENGINE_POOL_SIZE sets the pool size, and N8N_EXPRESSION_ENGINE_MAX_CODE_CACHE_SIZE bounds the AST cache.

The changes span workflow execution, live/test/waiting webhooks, and credential resolution paths, all now following the acquire-at-start, release-on-finish pattern. This is part of a broader initiative to bring VM-based expression isolation to n8n.

View Original GitHub Description

Summary

This PR adds isolate pooling to the VM expression engine, following an isolate-per-workflow approach: each workflow execution acquires one isolate at start, holds it for the execution's lifetime, and disposes it on completion. The pool pre-warms replacements in the background to eliminate cold-start latency.

  • Pool size matches the execution concurrency limit, so a warm bridge is always available when an execution starts.
  • Credential resolution (outside execution context) uses the same acquire/evaluate/release path; if the pool is exhausted it falls back to a cold-start bridge.
  • The Expression instance is the owner key — acquireIsolate()/releaseIsolate() are instance methods that use this as the WeakMap key, replacing the previous executionId-based Map.

Flow

sequenceDiagram
    participant WE as WorkflowExecute
    participant Expr as Expression (instance)
    participant Eval as ExpressionEvaluator
    participant Pool as IsolatePool
    participant Bridge as IsolatedVmBridge
    participant Isolate as V8 Isolate

    Note over WE,Isolate: Execution start
    WE->>Expr: acquireIsolate()
    Expr->>Eval: acquire(this)
    Eval->>Pool: acquire()
    Note over Pool: Pool size = concurrency limit,<br/>bridge always available
    Pool-->>Eval: bridge
    Pool->>Pool: replenish() in background
    Eval->>Eval: bridgesByOwner.set(expr, bridge)

    Note over WE,Isolate: Expression evaluation (sync, repeats per expression)
    WE->>Expr: renderExpression(expression, data)
    Expr->>Eval: evaluate(expression, data, this, { timezone })
    Eval->>Eval: getBridge(this) → bridgesByOwner.get(expr)
    Eval->>Eval: getTransformedCode(expression) → code cache or Tournament
    Eval->>Bridge: execute(transformedCode, data, { timezone })
    Bridge->>Isolate: script.runSync(wrappedCode)
    Isolate-->>Bridge: result
    Bridge-->>Eval: result
    Eval-->>Expr: result
    Expr-->>WE: result

    Note over WE,Isolate: Execution end (in .finally())
    WE->>Expr: releaseIsolate()
    Expr->>Eval: release(this)
    Eval->>Eval: bridgesByOwner.delete(expr)
    Eval->>Pool: release(bridge)
    Pool->>Bridge: dispose()
    Bridge->>Isolate: isolate.dispose()
    Note over Isolate: V8 heap freed

Related Linear tickets, Github issues, and Community forum posts

https://linear.app/n8n/issue/CAT-2279

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with Backport to Beta, Backport to Stable, or Backport to v1 (if the PR is an urgent fix that needs to be backported)
© 2026 · via Gitpulse