Limits

Set resource budgets for sandbox execution and host function traffic.

Limits

Limits bound the time, memory, data, and host interaction available to one invocation. They protect the host from accidental overuse and from source that tries to consume unbounded resources.

Concept

Every invocation has a RunLimits budget. The budget applies to the complete invocation, including guest execution, host function calls, result serialization, and continuation operations.

The defaults are a starting point, not a policy. A short data transformation and a coding agent rarely want the same budget.

Defaults

LimitDefaultBounds
timeoutMs30 secondsThe complete invocation
memoryLimitBytes64 MiBQuickJS and sync-bridge admission
maxStackSizeBytes2 MiBQuickJS stack
maxSourceBytes256 KiBEntry and loaded module source
maxResultBytes1 MiBSerialized result data
maxConsoleOutputBytes64 KiBGuest console output
maxHostFunctionArgumentsBytes1 MiBSerialized arguments for one call
maxHostFunctionOutputBytes4 MiBSerialized binding or loader output
maxBridgeRequests256Binding and module requests
maxInFlightBridgeRequests32Host function calls active at once
maxContinuationBytes32 MiBSerialized continuation state

Every configured value must be a positive integer.

Implementation

Use createRunner() when several invocations should share the same policy:

import { createRunner } from 'run';

const MiB = 1024 * 1024;

const runner = createRunner({
  limits: {
    timeoutMs: 15_000,
    memoryLimitBytes: 32 * MiB,
    maxStackSizeBytes: 1024 * 1024,
    maxSourceBytes: 128 * 1024,
    maxResultBytes: 512 * 1024,
    maxConsoleOutputBytes: 32 * 1024,
    maxHostFunctionArgumentsBytes: 256 * 1024,
    maxHostFunctionOutputBytes: MiB,
    maxBridgeRequests: 100,
    maxInFlightBridgeRequests: 8,
    maxContinuationBytes: 8 * MiB,
  },
});

Every runner.run() call inherits these values:

const result = await runner.run({
  source,
  hostFunctions,
});

An individual invocation can override only the values that need to change:

const result = await runner.run({
  source,
  hostFunctions,
  limits: {
    timeoutMs: 5_000,
    maxResultBytes: 128 * 1024,
  },
});

The five-second timeout and 128 KiB result budget replace the runner defaults for this invocation. Every other limit continues to come from the runner.

The convenience run() function also accepts per-invocation limits:

import { run } from 'run';

const result = await run({
  source,
  hostFunctions,
  limits: {
    timeoutMs: 5_000,
    memoryLimitBytes: 32 * 1024 * 1024,
  },
});

Selection

Start with the defaults and measure representative workloads before changing them. Lower a limit when the workload has a predictable shape, and raise it only when the application is prepared to absorb the additional resource use.

Source, argument, output, result, and continuation limits are measured after their relevant UTF-8 encoding or serialization step. A JavaScript object that appears small can have a much larger serialized representation.

For modules, maxSourceBytes applies both before and after type stripping, and maxHostFunctionOutputBytes applies to the JSON-encoded loader response written to the bridge. Escapes such as quotes and backslashes can make that encoded response larger than the source text.

Memory and worker concurrency should be planned together. Raising memoryLimitBytes increases the maximum memory available to every active worker, which may reduce the number of invocations a process can safely run at once.

Runs without synchronous bindings or a module loader do not allocate a synchronous bridge. When needed, the bridge is included in process memory admission, must fit within memoryLimitBytes, and has a hard 64 MiB capacity.

maxBridgeRequests controls total asynchronous bindings, synchronous bindings, and module-loader interaction, including requests rejected by the worker's name or argument size checks. Catching those errors does not restore the budget. Existing continuations created before aggregate attempt accounting retain their original counting rules. New continuations bind maxBridgeRequests and maxHostFunctionArgumentsBytes so those limits must match when resuming. Each module normalization and load counts as a separate request, while maxInFlightBridgeRequests controls parallel host interaction. Lowering the in-flight limit can protect a database or external service even when the total number of asynchronous host function calls remains unchanged. Synchronous and module requests are serialized separately and do not count toward the in-flight limit.

Failures

Limit violations reject the invocation. The most relevant stable codes are:

  • RUN_TIMEOUT indicates that the invocation exceeded timeoutMs.
  • RUN_SOURCE_TOO_LARGE indicates that the source exceeded maxSourceBytes.
  • RUN_BRIDGE_LIMIT indicates that total or in-flight host function traffic exceeded its configured limit.
  • RUN_HOST_FUNCTION_ERROR can indicate that host function arguments exceeded the host boundary or that a host function request was otherwise invalid.
  • RUN_SERIALIZATION_ERROR indicates that a transferred value was not serializable or exceeded another serialization boundary.
  • RUN_PROTOCOL_ERROR can indicate that continuation state or a continuation token exceeded maxContinuationBytes.

QuickJS memory and stack exhaustion do not have dedicated stable error codes. Treat their failures as runtime failures, monitor them, and adjust the budget only after confirming that the source is behaving as expected.

Use RunError.isInstance() and inspect error.code when the application needs different responses:

import { RunError } from 'run';

try {
  await runner.run({ source, hostFunctions });
} catch (error) {
  if (RunError.isInstance(error)) {
    console.error('Run failed', {
      code: error.code,
      details: error.details,
    });
  }

  throw error;
}

Do not automatically retry a limit violation with a larger budget. First determine whether the source is behaving as expected and whether host side effects are safe to repeat.