Files
skills/docs/api-stack/api/api-ws-bun/src/limits/tenant-cap.md
2026-05-29 08:20:56 +01:00

2.5 KiB

limits

(no submodules)

Purpose

Provides the per-tenant connection-count enforcement function that the handshake pipeline calls before admitting a new connection. The cap defends every host process against a single tenant exhausting socket capacity — a noisy-neighbour mitigation explicitly called out in design §11.4 (default maxConnectionsPerTenant=1000).

Enforcement is a function rather than a class because the only state needed is the registry itself; threading another stateful module through the handshake path would obscure the actual policy decision.

Features

  • O(1) cap check — leans on registry.countByTenant which is index-backed.
  • Throws WS_BUN.TENANT_CAP_EXCEEDED with the offending tenant id + configured cap in the message for debuggable telemetry.
  • Read-only — does not mutate the registry, so the handshake path can run additional checks after the cap test without rollback.

Usage: Basic

import { enforceTenantCap, BunConnectionRegistry } from "@suaveplan/api-ws-bun";

const registry = new BunConnectionRegistry();
enforceTenantCap(registry, tenantId, 1000); // throws if >= 1000.

Usage: Advanced

import {
    enforceTenantCap,
    BunConnectionRegistry,
    WS_BUN_CODES,
} from "@suaveplan/api-ws-bun";

function tryAccept(reg: BunConnectionRegistry, tenant, cap) {
    try {
        enforceTenantCap(reg, tenant, cap);
        return { accepted: true as const };
    } catch (err) {
        if ((err as { code?: string }).code === WS_BUN_CODES.TENANT_CAP_EXCEEDED) {
            return { accepted: false as const, httpStatus: 429 };
        }
        throw err;
    }
}

API Reference

  • enforceTenantCap(registry, tenant, cap): void — throws on cap breach; otherwise no-op.

Implementation Notes

The check uses >= rather than > so a cap of 1000 admits exactly 1000 concurrent connections; the 1001st gets the 429. This matches the spec scenario verbatim and avoids the off-by-one trap of "999 admitted, 1000 rejected".

The implementation does not reserve a registry slot before the cap test. The window between the check and registry.add(conn) is bounded by a synchronous function call in the engine, so racing two handshakes through that microsecond gap would only be possible if Bun.serve fired two upgrade callbacks on the same JavaScript turn — which it does not. If a future engine extends the gap, this function should be replaced with an atomic reserveSlot / releaseSlot pair.