Overview
Multi-tenant connection routing primitive for Bun PaaS gateways. Sits in front of N backend processes (each a @absolutejs/runtime instance hosting a @absolutejs/sync engine for a subset of tenants) and decides — per request:
Per-tenant connection routing for the gateway in front of your Bun backends. Sits between an incoming request / WS upgrade and N backend processes; decides which shard owns the tenant, whether the tenant is over its rate limit, whether the tenant is over its connection cap, and whether the chosen shard is healthy and not draining. Pure logic — wire it into whatever HTTP/WS layer you have.
Every route call applies the same ordered gates so quota, capacity, health, and shard ownership cannot disagree.
route() returns { decision, shard, emptiedBucket? } where decision is one of allow / rate-limited / capped / no-shards / denied. The gateway forwards bytes to shard.url on allow; everything else is a 4xx.
import { createRouter } from '@absolutejs/router';
const router = createRouter({
shards: [
{ id: 'engine-1', url: 'ws://10.0.0.11:3000' },
{ id: 'engine-2', url: 'ws://10.0.0.12:3000' },
],
hashStrategy: 'jump', // default — exact 1/N movement
perTenantConnectionCap: 100,
perTenantRateLimit: { tokens: 100, refillPerSecond: 10 },
});
// In your WS upgrade handler:
const decision = router.route({ tenantId, channelId });
if (decision.decision !== 'allow') {
return new Response(decision.decision, { status: 429 });
}
const handle = router.acquire(tenantId);
// ...proxy WS frames to decision.shard.url; call handle.release() on close.Default is jump-consistent-hash (Lamping & Veach 2014) — O(log n), no memory, exactly 1/N keys move on a shard-add at the tail. Rendezvous (HRW) supports per-shard weights for heterogeneous engine sizes AND a per-call load(shardId) hook to bias AWAY from hot shards. Both strategies are sticky — the same tenant always lands on the same shard until membership changes.
// jump (default) — Lamping & Veach 2014. O(log n), no memory, exactly
// 1/N keys move when shards are added at the tail. Ignores weight.
createRouter({ shards, hashStrategy: 'jump' });
// rendezvous — HRW hash. Supports per-shard weight for heterogeneous
// engine sizes. O(N) per lookup. Also accepts a load() hook biasing
// AWAY from overloaded shards: effectiveWeight = weight / load(id).
createRouter({
shards: [
{ id: 'big', url: 'ws://...', weight: 8 },
{ id: 'small', url: 'ws://...', weight: 1 },
],
hashStrategy: 'rendezvous',
load: (id) => runtimeRoster.get(id)?.activeTenants ?? 1,
});
// Custom strategy: (key, healthyShards) => index
createRouter({
shards,
hashStrategy: (key, shards) => fnv1a32(key) % shards.length,
});drainShard(id) is the operator-intentional "finishing up" state — distinct from markUnhealthy(id) which means "broken, route around now." Both exclude the shard from new routing; drain leaves existing acquires alone. Use drain before a planned shard reboot; tenants rehash to healthy non-draining shards on their next route, but in-flight requests aren't torn down.
// drainShard(id) excludes a shard from new routing without marking it
// broken. Existing acquires keep running. For planned shard rotation:
router.drainShard('engine-1');
// Wait for the runtime to report 0 active tenants on that shard, then:
router.removeShard('engine-1');
// markUnhealthy(id) is the failure variant — tenants rehash to a
// healthy shard immediately. markHealthy(id) clears BOTH states.perTenantConnectionCap counts active connections via acquire() / release(); over the cap, route() returns capped. The token-bucket perTenantRateLimit gates per-call; perRouteRateLimits layers a SECOND bucket per named route with atomic two-bucket commit (a failed route bucket does NOT consume the tenant bucket).
// Per-tenant connection cap counted via acquire() / release().
// When reached, route() returns { decision: 'capped' }.
const router = createRouter({
shards,
perTenantConnectionCap: 100,
perTenantRateLimit: { tokens: 100, refillPerSecond: 10 },
// Per-route limits layered on top — atomic two-bucket commit. A failed
// route bucket does NOT consume the tenant bucket.
perRouteRateLimits: {
upload: { tokens: 5, refillPerSecond: 0.083 }, // 5/min
},
});
router.route({ tenantId: 'acme', route: 'upload' });
// → { decision: 'rate-limited', emptiedBucket: 'upload' } after 5 callsallow()The allow: (tenantId) => boolean hook is the meter+router wire-up in one line: allow: meter.allow. When the meter has tripped a tenant, the router returns denied at the edge — the gateway can surface "quota exceeded" without paying for the upstream hop.
import { createMeter } from '@absolutejs/metering';
import { createRouter } from '@absolutejs/router';
const meter = createMeter({ /* ... */ });
// The allow hook is the meter+router wire-up in one line.
const router = createRouter({
shards,
allow: meter.allow, // refuse routes for over-quota tenants at the edge
load: (id) => roster.load(id),
});
// route() now returns { decision: 'denied', shard: null } when meter.allow
// returns false. The gateway can surface a 'quota exceeded' page without
// paying for the upstream hop.Preserve rate-limit tokens across edge restarts. Without this, a deploy hands every tenant a fresh full bucket — instant rate-limit bypass for anyone watching the deploy times.
// Preserve rate-limit tokens across edge restarts. Without this, a deploy
// hands every tenant a fresh full bucket — instant rate-limit bypass for
// anyone watching the deploy times.
const snap = router.snapshot();
await Bun.write('/var/lib/router/state.json', JSON.stringify(snap));
// On edge restart:
const restored = createRouter({ /* ... same config ... */ });
restored.restore(JSON.parse(await Bun.file('/var/lib/router/state.json').text()));Current package surface
Import surface · click to copy
type Region = {
id: string;
/**
* Relative weight for the default assignment strategy (weighted
* rendezvous). Higher weight = proportionally more tenants land here.
* Default 1. Weights <= 0 exclude the region from default assignment
* (explicit `assignRegion` still works).
*/
weight?: number;
};@absolutejs/routerOutcomes
Multi-tenant connection routing primitive for Bun PaaS gateways. Sits in front of N backend processes (each a @absolutejs/runtime instance hosting a @absolutejs/sync engine for a subset of tenants) and decides — per request:
API — Purpose
createRouter shards WITHIN a region; createRegionDirectory decides which region a tenant lives in. Sticky, deterministic assignment — weighted rendezvous over region ids by default, so every replica computes the same answer without coordination — plus an optional caller hook for latency-based placement and explicit overrides for control-plane onboarding decisions.
Hardening checklist
Follow in order