dotnet/runtime · error · Error

Out of scratch root space

Error message

Out of scratch root space

What it means

Thrown by _mono_wasm_claim_scratch_index when the global scratch-root pool is exhausted. The runtime pre-allocates a fixed pool of maxScratchRoots (8192) slots; each mono_wasm_new_root that does not hit the recycle pool claims one, and once the free-indices count drops below 1, no more scratch roots can be handed out until some are released.

Source

Thrown at src/mono/browser/runtime/roots.ts:156

        return;

    _scratch_root_buffer!.set(index, <any>0);
    _scratch_root_free_indices![_scratch_root_free_indices_count] = index;
    _scratch_root_free_indices_count++;
}

function _mono_wasm_claim_scratch_index () {
    if (is_nullish(_scratch_root_buffer) || !_scratch_root_free_indices) {
        _scratch_root_buffer = mono_wasm_new_root_buffer(maxScratchRoots, "js roots");

        _scratch_root_free_indices = new Int32Array(maxScratchRoots);
        _scratch_root_free_indices_count = maxScratchRoots;
        for (let i = 0; i < maxScratchRoots; i++)
            _scratch_root_free_indices[i] = maxScratchRoots - i - 1;
    }

    if (_scratch_root_free_indices_count < 1)
        throw new Error("Out of scratch root space");

    const result = _scratch_root_free_indices[_scratch_root_free_indices_count - 1];
    _scratch_root_free_indices_count--;
    return result;
}

export class WasmRootBufferImpl implements WasmRootBuffer {
    private __count: number;
    private length: number;
    private __offset: VoidPtr;
    private __offset32: number;
    private __handle: number;
    private __ownsAllocation: boolean;

    constructor (offset: VoidPtr, capacity: number, ownsAllocation: boolean, name?: string) {
        const capacityBytes = capacity * 4;

        this.__offset = offset as any >>> 0 as any;

View on GitHub (pinned to 60108ba66e)

Solutions

  1. Always pair mono_wasm_new_root with release() - prefer try/finally blocks calling mono_wasm_release_roots.
  2. Batch roots into a single mono_wasm_new_root_buffer when you need many, instead of thousands of individual scratch roots.
  3. Find the leak: count new-root vs release-root calls in your hot path and balance them.
  4. Reduce peak simultaneous root count by releasing roots as soon as the interop call completes.

Example fix

// before
for (const item of items) {
  const r = mono_wasm_new_root(item.ptr);
  // ... never released
}
// after
for (const item of items) {
  const r = mono_wasm_new_root(item.ptr);
  try { /* use r */ } finally { r.release(); }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Track live roots and bail before exceeding the 8192 scratch pool.
let liveRoots = 0;
const MAX_SCRATCH = 8192;
function tryNewRoot<T extends MonoObject>(value?: number) {
  if (liveRoots >= MAX_SCRATCH) return null;
  const r = mono_wasm_new_root(value);
  liveRoots++;
  const origRelease = r.release.bind(r);
  r.release = () => { origRelease(); liveRoots--; };
  return r;
}

Try / catch

try {
  const r = mono_wasm_new_root(value);
  // ... work
} catch (e) {
  if ((e as Error).message === 'Out of scratch root space') {
    // force-release pooled roots, or batch into a root buffer instead
  }
  throw e;
} finally {
  mono_wasm_release_roots(r);
}

Prevention

When it happens

Trigger: Calling mono_wasm_new_root / mono_wasm_new_roots more than 8192 times concurrently without releasing roots; or a root leak where release() is never called, steadily draining the pool until it is empty.

Common situations: Interop code that allocates roots in a loop without releasing them; long-running handlers that hold many roots simultaneously; a finally block that should call mono_wasm_release_roots but is missing or skipped on an error path.

Related errors


AI-assisted analysis of dotnet/runtime@60108ba66e (2026-08-10). Data as JSON: /api/errors/5ce1ce8225e7c812. Report an issue: GitHub.