Skip to content

Self-host: a CPU-bound execute blocks the whole server for up to 5 min (QuickJS runs on the main thread) #1616

Description

@salmonumbrella

The situation

Self-host runs execute in QuickJS on the server's main thread. apps/host-selfhost/src/execution.ts wires makeQuickJsExecutor() with defaults, runtime-quickjs evaluates with the sync WASM build, and the only bound is the in-sandbox interrupt handler at timeoutMs = 300 000. So a CPU-bound script owns the Node event loop until it finishes or hits the 5 min deadline. Nothing else on the instance is served in the meantime.

Measured on the official v1.5.40 image, Linux amd64, one MCP client:

  • execute with while (Date.now() - t0 < 150000) {} returned 200 after 150.3 s.
  • during those 150 s, /api/health took 3.6 s and a concurrent GET /api/oauth/clients got no answer within 30 s. The web UI did not load either.
  • same script at 75 s: same behaviour, returned 200 after 75.6 s.

I/O-bound scripts are fine — every tools.* call yields to the host, and since #1437 the deadline is suspended while a dispatch is in flight — so normal agent code never triggers this. It takes one careless loop (spin-poll on a job, an accidental O(n²) over a big payload) to stall a shared instance for up to five minutes for every user and every MCP client on it.

What works today, and why it is not enough

  • The 5 min interrupt eventually kills the script. That is a ceiling, not isolation.
  • Speed up and stabilize CI e2e #1551 adds EXECUTOR_SANDBOX_TIMEOUT_MS to self-host. I'll set it to ~60 s the day it ships; it shortens the stall, it does not remove it.
  • Cloud does not have the problem because it runs code in dynamic workers, isolated from the API process. runtime-deno-subprocess exists in the repo but no app depends on it.

Ask

Run the self-host sandbox off the main thread: a worker_threads worker around the existing sync QuickJS build would be enough (or the asyncify variant with a periodic yield, or the Deno subprocess runtime). Keep the interrupt deadline as the backstop. Merging #1551 with a lower default than 5 min would help in the meantime.

Not asking for per-execution CPU accounting or resource quotas here — just that one execute cannot take the whole server with it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions