windmill-labs/windmill · error

The expression evaluation `{expr}` took too long to execute

Error message

The expression evaluation `{expr}` took too long to execute (>{EVAL_TIMEOUT_MS}ms)

What it means

A flow step-input transform expression evaluated with QuickJS exceeded the hard wall-clock timeout (EVAL_TIMEOUT_MS). The tokio select/timed wrapper aborted the evaluation and returned this error instead of the expression result.

Source

Thrown at backend/windmill-jseval/src/lib.rs:325

            handle.block_on(async move {
                eval_quickjs_inner(
                    &expr_clone,
                    filtered_context,
                    flow_input_clone,
                    flow_env_clone,
                    authed_client_clone,
                    by_id_clone,
                    ctx,
                    context_keys,
                    memory_limit,
                )
                .await
            })
        }),
    )
    .await
    .map_err(|_| {
        anyhow::anyhow!(
            "The expression evaluation `{expr}` took too long to execute (>{EVAL_TIMEOUT_MS}ms)"
        )
    })??
}

/// Default memory cap, in bytes, for a single flow step-input transform eval
/// (`eval_timeout_quickjs`). Large enough for transforms that build sizeable
/// arrays; genuinely large payloads raise it via `QUICKJS_MEMORY_LIMIT_MB`.
///
/// Sizing constraint: evals are authenticated and, within a worker process, run
/// one at a time (`transform_input` awaits each transform sequentially and each
/// eval drops its runtime before the next), so in the default one-worker-per-
/// process deployment the peak is a single cap. Native / multi-worker-in-process
/// mode runs up to `NUM_WORKERS` evals concurrently in one heap, so the peak is
/// `NUM_WORKERS × cap` — hence a modest default rather than a large one.
#[cfg(feature = "quickjs")]
const DEFAULT_QUICKJS_MEMORY_LIMIT_BYTES: usize = 128 * 1024 * 1024;

View on GitHub (pinned to e474e8803c)

Solutions

  1. Reduce the amount of data the expression handles — filter/map earlier in the flow instead of inside the transform
  2. Rewrite the expression to avoid unbounded loops and quadratic operations
  3. Move heavy processing into a proper script step rather than a step-input transform
  4. Raise the eval timeout via the EVAL_TIMEOUT_MS environment variable if legitimately slow but bounded

Example fix

// before
results.items.map(x => expensiveTransform(x))  // O(n^2), times out
// after
results.items.filter(x => x.keep).map(x => expensiveTransform(x))
Defensive patterns

Strategy: try-catch

Validate before calling

// bound work before submitting the expression
if (results.items.length > 10000) throw new Error('too many items for transform');

Try / catch

try {
  const out = await evalExpression(expr, inputs);
} catch (e) {
  if (String(e).includes('took too long to execute')) {
    // fall back to a dedicated script step instead of the transform
  }
  throw e;
}

Prevention

When it happens

Trigger: An expression like `JSON.stringify(results.step)` on a very large payload, an accidental infinite loop (e.g. `while(true)` or O(n^2) mapping over a big array) inside a step-input transform, or a transform referencing a huge previous-step result that takes longer than the timeout to process.

Common situations: Passing megabyte-scale payloads through flow input transforms; authoring a transform with an unbounded loop; a slow string-building expression over thousands of rows.

Understand the failure class

Related errors


AI-assisted analysis of windmill-labs/windmill@e474e8803c (2026-09-03). Data as JSON: /api/errors/76f56fa5f12698f2. Report an issue: GitHub.