AbsoluteJS

Runtime

Run many isolated Bun apps on one host. Wraps Bun.spawn so that "run this tenant's bun run start inside an idle-killing, metric-emitting child process" is one function call. Built for teams running many small Bun apps in production on their own infrastructure.

#Quick Start

createRuntime() returns a pool. The firstensure(key) call spawns the tenant's process, waits for HTTP readiness on the bound port, and returns the live Tenant handle. Subsequent calls reuse the running process; touch(key)defers the idle-kill clock.

TS
import { createRuntime } from '@absolutejs/runtime';

const runtime = createRuntime({
  source: { kind: 'directory', root: '/srv/tenants' },
  idleAfterMs: 5 * 60 * 1000,                  // kill processes idle 5 min
  maxConcurrent: 100,                          // LRU-evict past 100 running
  onMetrics: (event) => prometheus.observe(event),
  onLog: (event) => loki.write(event),
  onTransition: (event) => audit.write(event),
});

// First call: spawns 'bun run start' in /srv/tenants/tenant-42, injects
// PORT, waits for readiness, returns the bound port.
const tenant = await runtime.ensure('tenant-42');
await fetch(`http://127.0.0.1:${tenant.port}/`);

// Subsequent calls reuse the running process; touch() defers idle-kill.
runtime.touch('tenant-42');

runtime.stats();             // { running, total, draining, backoff }
await runtime.dispose();     // kill all + stop the sweep

#Hibernation

Hibernation is idle-kill at the process layer. Bun has no shipped process-level snapshot/resume primitive as of 2026; when it lands, an opt-inhibernate: 'process-snapshot' mode joins idle-kill as the default. The current default trades wake latency (50-200ms cold spawn) for multi-tenant economics — a thousand idle tenants cost a small finite RSS, not a thousand running processes.

TS
// v0.1.0 hibernation strategy: idle-kill at the process layer.
//
// Bun has no shipped process-level snapshot/resume primitive as of 2026.
// The trade-off the default makes explicit: first call after idle pays a
// full Bun cold spawn (~50-200ms). For multi-tenant economics with mostly-
// idle tenants, that's the right default.
//
// JSC-context hibernation comes for free if the customer's process runs
// sandboxed handlers via @absolutejs/isolated-jsc — that layer hibernates
// per-context, not per-process, and the runtime gates the surrounding
// process at a coarser grain.
//
// If wake latency matters for your workload, disable idle-kill and rely
// on LRU eviction at maxConcurrent:
const runtime = createRuntime({
  source: { kind: 'directory', root: '/srv/tenants' },
  idleAfterMs: 0,             // disabled — only LRU + explicit kill shed
  maxConcurrent: 50,
});

#Spawn Back-off

A spawn that fails (the spawn fn threw, readiness timed out) records per-key { attempt, retryAt, lastError } and the next ensure(key) throws fast until retryAt. After maxFailures consecutive failures, the key stays refused until clearBackoff(key). Without this, a broken tenant thrashes the host with rapid spawn retries — exactly the pattern that takes down nearby tenants.

TS
// A spawn that fails (the spawn fn threw, or readiness timed out) records
// a per-key { attempt, retryAt, lastError } and the next ensure(key) throws
// fast until retryAt. After maxFailures consecutive failures, the key stays
// refused until clearBackoff(key). Without this, one broken tenant thrashes
// the host with rapid spawn retries.
const runtime = createRuntime({
  source: { kind: 'directory', root: '/srv/tenants' },
  backoff: {
    baseMs:      1_000,       // first retry waits 1s
    maxMs:       60_000,      // cap at 1 min
    maxFailures: 10,          // refuse outright after 10
  },
});

await runtime.ensure('broken-tenant'); // fails, records backoff
// Immediate retry refused without invoking spawn — no thrash.
await runtime.ensure('broken-tenant'); // throws 'backing off after 1 failure(s)'

// Operator fixes the underlying issue, then:
runtime.clearBackoff('broken-tenant');
await runtime.ensure('broken-tenant'); // retries now

#Restart, Drain & Exit Reasons

restart(key) kills + spawns fresh in one call — for deploys that swap a tenant to a new release. drain() refuses new ensure() spawns while existing tenants keep running, for graceful shard shutdown. Every exit transition carries a structured reason so your metering or ops tooling can decide whether to charge, retry, or alert.

TS
// restart(key) — kill + spawn fresh in one call. Used by deploys to swap
// to a new release after the 'current' symlink moves.
const fresh = await runtime.restart('tenant-42');

// drain() — refuse new ensure() spawns; existing tenants keep running.
// Used for graceful shard shutdown before a host reboot.
runtime.drain();
runtime.stats().draining;     // true
// Operator waits for stats().running to reach 0, then dispose().

// Structured exit reasons surface on the 'exit' transition event:
//   crashed | exited-clean | idle-killed | lru-evicted | killed |
//   readiness-timeout | disposed | restarted
//
// The meter / control plane decides whether to charge (clean), retry
// (crash), or alert (readiness-timeout) based on this field.

#CPU + RSS Observation

On Linux, the sweeper reads /proc/<pid>/stat (utime + stime) and /proc/<pid>/status (VmRSS) per running tenant on a configurable interval and emits { type: 'observation', cpuMs, rssBytes } via onMetrics. This is the per-tenant data @absolutejs/metering consumes to attribute idle hibernation cost precisely — without it, processCpuMs would always be zero.

TS
// Linux /proc-derived CPU + RSS observation (the data @absolutejs/metering
// needs to attribute idle hibernation cost precisely).
const runtime = createRuntime({
  source: { kind: 'directory', root: '/srv/tenants' },
  observeIntervalMs: 30_000,     // every 30s, default
  onMetrics: (event) => {
    if (event.type === 'observation') {
      // event = { type, key, pid, cpuMs, rssBytes, at }
      meter.record({
        type: 'observation',
        tenant: event.key,
        cpuMs: event.cpuMs,
        rssBytes: event.rssBytes,
        at: event.at,
      });
    }
  },
});

// cpuMs is CUMULATIVE since spawn — the meter charges the delta from the
// previous observation. A spawn/exit transition resets the baseline so a
// fresh process doesn't double-charge. Linux-only; the sweeper silently
// skips on macOS / Windows.