Concurrency
Control parallel host function calls and simultaneous sandbox invocations.
Concurrency
Concurrency appears at two levels in run. Guest source can call several host
functions at once, and the host process can execute several sandbox invocations
at once.
These levels use different limits and should be configured independently.
Concept
A host function call is a request from the guest to the host. Guest source can
create parallel host function requests with Promise.all(). The
maxInFlightBridgeRequests limit controls how many of those requests may
remain active at once.
Each active sandbox invocation occupies one worker from the process-wide worker pool. The worker cap controls how many invocations may execute simultaneously across every runner in the process.
run does not maintain an unbounded waiting queue. When the worker cap is
reached, a new invocation rejects with RunConcurrencyError.
Host functions
Use parallel host function calls when the operations are independent:
const result = await run({
source: `
const [profile, orders] = await Promise.all([
users.getProfile("user_123"),
orders.listForUser("user_123"),
]);
return { profile, orders };
`,
hostFunctions,
limits: {
maxInFlightBridgeRequests: 8,
},
});Both host functions execute in the host. The guest waits until both have settled before returning.
The default in-flight limit is 32, and the default total host-function-call
limit is 256. Exceeding either limit rejects the invocation with
RUN_BRIDGE_LIMIT.
Lower the in-flight limit when host functions use a constrained dependency such as a database connection pool. The host function limit should not exceed the capacity that the host service can safely absorb.
Workers
The default worker cap adapts to available process memory. It admits at least one invocation and caps the dynamic default at 32 active workers.
Use setMaxWorkers() when the application needs an explicit process-wide cap:
import { setMaxWorkers } from 'run';
// Configure this once during process startup.
setMaxWorkers(8);The cap applies to every run() call and every runner in the process. It is
not scoped to the module or runner that called setMaxWorkers().
Calling the function without a value restores the dynamic memory-based default:
setMaxWorkers();Choose the cap with both QuickJS memory and worker overhead in mind. A worker
can consume more memory than the configured guest memoryLimitBytes, so
memoryLimitBytes is not the total cost of an invocation.
Admission
Decide what happens when all workers are busy. An HTTP service can reject the request with backpressure:
import { RunError } from 'run';
try {
const result = await runner.run({ source, hostFunctions });
return Response.json(result);
} catch (error) {
if (RunError.isInstance(error) && error.code === 'RUN_CONCURRENCY_LIMIT') {
return new Response('Runtime is busy.', {
status: 503,
headers: {
'Retry-After': '1',
},
});
}
throw error;
}A background system can instead place work in its existing durable job queue
before calling the runner. The queue should live in the host application;
run intentionally does not retain pending invocations.
Avoid immediate retry loops around RUN_CONCURRENCY_LIMIT. They add load
without creating capacity. Apply backoff, return backpressure, or enqueue the
work through an application-controlled scheduler.
Capacity
Host function concurrency and worker concurrency multiply each other. Eight active workers with an in-flight host function limit of eight can produce up to 64 simultaneous host operations.
Set both values from the capacity of downstream services, then measure:
- Active invocations and concurrency-limit failures.
- Active host function calls and their latency.
- Process memory under representative source and result sizes.
Under memory pressure, reduce concurrency before raising memory limits.
Concurrency only improves throughput while the host and its dependencies have capacity. Past that, it adds latency and failures.