---
title: Router
description: route and the Router/Membership/hashing internals behind cache-affinity routing, hedging, and failover.
---

```ts
import { route } from "lakefront/router";
```

The cluster front door: deterministic cache-affinity routing, hedging, and failover across a fixed set of server nodes.

## Functions

### route

```ts
function route(options: RouteOptions): Promise<RouterServer>;
```

| Parameter | Type           | Default    | Description                        |
| --------- | -------------- | ---------- | ---------------------------------- |
| `options` | `RouteOptions` | (required) | See [RouteOptions](#routeoptions). |

**Returns** the running [RouterServer](#routerserver). Probes membership once, starts the health-poll loop, and opens the HTTP front door.

```ts
const router = await route({
  nodes: [
    { id: "node-a", url: "http://127.0.0.1:58001" },
    { id: "node-b", url: "http://127.0.0.1:58002" },
    { id: "node-c", url: "http://127.0.0.1:58003" },
  ],
  port: 8080,
});
```

### quantiseWeight

```ts
function quantiseWeight(raw: number): number;
```

Snaps a raw capacity number to the nearest [`WEIGHT_LEVELS`](#weight_levels) band (`<= 0` maps to `0`). Keeping weight banded stops routing from oscillating on every small capacity change.

### rank

```ts
function rank(nodes: readonly NodeInfo[], boardId: string): NodeInfo[];
```

All eligible (`weight > 0`) nodes for `boardId`, best first; ties broken by `id` lexical order. The head is the board's native node, the rest are failover order.

### score

```ts
function score(node: NodeInfo, boardId: string): number;
```

`-weight / ln(u)`, where `u = hash(node, board)` is normalised to `(0,1)` via 64-bit wyhash: gives each node a selection probability exactly proportional to its weight. `weight <= 0` scores `-Infinity`.

### isIdempotentPath

```ts
function isIdempotentPath(path: string): boolean;
```

Whether `path` names a procedure in [`IDEMPOTENT_PROCEDURES`](#idempotent_procedures). Re-exported from `lakefront/contract`, where it is defined, for one import path.

### isRetryable

```ts
function isRetryable(path: string, headers?: Headers): boolean;
```

`isIdempotentPath(path)`, or `headers` carrying [`IDEMPOTENT_HEADER`](#idempotent_header) set to `"1"`. Re-exported from `lakefront/contract`.

## Classes

### Router

`Router` is the class `route` wires up; construct it directly to embed routing in your own HTTP layer.

```ts
const router = new Router(options: RouterOptions);
```

See [RouterOptions](#routeroptions).

#### route

```ts
route(boardId: string): NodeInfo[]
```

Deterministic node preference for a board, best first: every router instance computes the same ranking from the same membership, with no coordination.

#### forward

```ts
forward(
  boardId: string,
  path: string,
  body: string,
  retryable?: boolean,
  authorization?: string,
): Promise<ForwardResult>
```

| Parameter       | Type      | Default                  | Description                                               |
| --------------- | --------- | ------------------------ | --------------------------------------------------------- |
| `boardId`       | `string`  | (required)               | Board to route.                                           |
| `path`          | `string`  | (required)               | Request path, e.g. `/rpc/query.execute`.                  |
| `body`          | `string`  | (required)               | Raw request body to forward.                              |
| `retryable`     | `boolean` | `isIdempotentPath(path)` | Whether a failure may hedge and fail over to other nodes. |
| `authorization` | `string`  | `undefined`              | Forwarded verbatim, never parsed.                         |

Sends to the board's native node, hedging to the next-ranked node if the first has not answered by its own p95 (or `hedgeDelayMs`, before it has 8 samples). A non-retryable call (a bare mutation, no `idempotencyKey`) gets exactly one attempt on exactly one node: no hedge, no failover. Hedging or retrying a write that may have already landed risks a second copy that nothing downstream could undo, since the lake is append-only; a keyed mutation deduplicates at the WAL and can be hedged and retried on another node. Design and rationale are covered under [routing](/concepts/routing).

#### counters

```ts
readonly counters: { requests: number; hedges: number; hedgeWins: number; failovers: number };
```

Running totals, mutated as `forward` runs.

### Membership

```ts
const membership = new Membership(options: MembershipOptions);
```

See [MembershipOptions](#membershipoptions).

#### nodes

```ts
nodes(): NodeInfo[]
```

Every configured member with its current weight: unhealthy members carry weight `0`, which removes them from ranking without renumbering anything else.

#### all

```ts
all(): readonly MemberConfig[]
```

Every configured member, healthy or not.

#### healthyIds

```ts
healthyIds(): string[]
```

Ids currently marked healthy.

#### probeOnce

```ts
probeOnce(): Promise<void>
```

Hits `GET /health` on every member once, in parallel, with `timeoutMs`.

#### start

```ts
start(): void
```

Starts the `intervalMs` poll loop.

#### stop

```ts
stop(): void
```

Stops the poll loop.

#### setCapacity

```ts
setCapacity(id: string, capacity: number): void
```

Feeds a capacity signal in, quantised at `nodes()` time. Kept separate from health so a slow node is de-preferred, not removed.

#### markUnhealthy

```ts
markUnhealthy(id: string): void
```

Marks a member unhealthy immediately, outside the poll cadence.

### LatencyWindow

Per-node latency tracking used to set the hedge delay: hedging at a node's own recent p95 keeps roughly 5% of requests hedged in steady state rather than over- or under-hedging on a fixed timeout.

```ts
const window = new LatencyWindow();
```

#### record

```ts
record(nodeId: string, ms: number): void
```

Appends a sample to a 128-entry sliding window per node.

#### percentile

```ts
percentile(nodeId: string, p: number): number | undefined
```

`undefined` until the node has 8 or more samples: callers fall back to a configured default rather than hedging on noise.

#### count

```ts
count(nodeId: string): number
```

Samples currently held for a node.

## Types

### NodeInfo

```ts
interface NodeInfo {
  readonly id: string;
  readonly url: string;
  readonly weight: number; // 0 excludes the node entirely
}
```

### RouteOptions

| Field              | Type                      | Default    | Notes                                                                 |
| ------------------ | ------------------------- | ---------- | --------------------------------------------------------------------- |
| `nodes`            | `readonly MemberConfig[]` | (required) | `{ id, url }` per server node.                                        |
| `port`             | `number`                  | `0`        | `0` asks the OS to pick a free port.                                  |
| `hedgeDelayMs`     | `number`                  | `250`      | Fallback hedge delay before a node has 8+ latency samples of its own. |
| `healthIntervalMs` | `number`                  | `1000`     | Health-poll interval: forwarded to `Membership`.                      |

### RouterServer

| Field        | Type                                                                   |
| ------------ | ---------------------------------------------------------------------- |
| `port`       | `number`                                                               |
| `url`        | `string`                                                               |
| `router`     | `Router`                                                               |
| `membership` | `Membership`                                                           |
| `stop`       | `(): Promise<void>`: stops the health-poll loop, then the HTTP server. |

### RouterOptions

```ts
interface RouterOptions {
  readonly membership: Membership;
  readonly hedgeDelayMs?: number; // default 250
  readonly hedgeAfterPercentile?: number; // default 95
  readonly requestTimeoutMs?: number; // default 10_000
  readonly fetch?: typeof globalThis.fetch;
}
```

### ForwardResult

```ts
interface ForwardResult {
  readonly status: number;
  readonly body: string;
  readonly servedBy: string;
  readonly hedged: boolean;
  readonly attempted: readonly string[];
  readonly latencyMs: number;
}
```

### MemberConfig

```ts
interface MemberConfig {
  readonly id: string;
  readonly url: string;
}
```

### MembershipOptions

```ts
interface MembershipOptions {
  readonly members: readonly MemberConfig[];
  readonly intervalMs?: number; // default 1000
  readonly timeoutMs?: number; // default 500
  readonly fetch?: typeof globalThis.fetch;
}
```

## Constants

### WEIGHT_LEVELS

`[0.25, 0.5, 1, 1.5, 2]`. Quantisation bands `quantiseWeight` snaps to.

### IDEMPOTENT_HEADER

`"x-lakefront-idempotent"`. Re-exported from `lakefront/contract`, where it is defined.

### IDEMPOTENT_PROCEDURES

`ReadonlySet<string>` of `{"query.execute", "node.stats", "changes.read"}`. Re-exported from `lakefront/contract`, where it is defined.

## HTTP surface

| Route               | Method | Response                                                                                                                                                                                                                   |
| ------------------- | ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `/health`           | `GET`  | `{ ok: true, healthy: string[] }`: currently healthy member ids.                                                                                                                                                           |
| `/route?board=<id>` | `GET`  | `{ board, ranking: { id, weight }[], healthy: string[] }`: introspection of the rendezvous ranking for a board, without forwarding anything.                                                                               |
| `/stats`            | `GET`  | `{ requests, hedges, hedgeWins, failovers, healthy }`: the router's counters plus currently healthy member ids.                                                                                                            |
| `/rpc/*`            | `POST` | Proxied to the board's ranked node(s). Requires header `x-lakefront-board`, `401` without it. Responds with `x-lakefront-served-by` and `x-lakefront-hedged`; see the [wire protocol reference](/reference/wire-protocol). |
| anything else       | any    | `404`, `{ "error": "not found" }`.                                                                                                                                                                                         |

## Standalone entrypoint

The container entrypoint (`router/main.ts`) reads its configuration from the environment: `LAKEFRONT_NODES`, `LAKEFRONT_HEDGE_MS`, `PORT`. See the [environment variable reference](/reference/environment). `route` itself takes options directly and reads no env.
