net.device-status
Classify a device as up, degraded or down from its latency summary and loss, p95 and jitter thresholds.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 22 tests, run in TypeScript, Python and Rust.
What it does
Decides whether a monitored device is **up**, **degraded** or **down** from its `net.latency-summary`, and says why, so an alert and a dashboard never disagree about what "degraded" means.
## Rules, in order
For example
deviceStatus(sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin…)→ status up, reasons a healthy link is up with no reasonsdeviceStatus(sent 3, received 0, lost 3, loss basis points 100%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 u…)→ status down, reasons no replies: 3 of 3 probes lost no replies at all is downdeviceStatus(sent 3, received 1, lost 2, loss basis points 66.67%, min us 700, avg us 700, max us 700, p95 us 700, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degr…)→ status down, reasons loss 66.67% at or above the down threshold 50.00% loss above the down threshold is down
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function deviceStatus(summary: LatencySummary, thresholds: StatusThresholds): StatusResult
| summary | LatencySummary | |
| thresholds | StatusThresholds | |
| returns | StatusResult |
The types it declares, generated into your project
/** When a device stops counting as healthy. */
export interface StatusThresholds {
/** loss at or above this is degraded, at least 1 */
readonly degradedLossBasisPoints: number;
/** loss at or above this is down, up to 10000 */
readonly downLossBasisPoints: number;
/** p95 at or above this many microseconds is degraded */
readonly degradedP95Us: number;
/** jitter at or above this is degraded; null to ignore jitter */
readonly degradedJitterUs: number | null;
}
export type DeviceStatus = "up" | "degraded" | "down";
/** The status and every reason for it, in a fixed order. */
export interface StatusResult {
readonly status: DeviceStatus;
/** empty when up */
readonly reasons: readonly string[];
}
Your code names it in one line, in the file that uses it
import { deviceStatus } from "#fune/net.device-status@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { type LatencySummary } from "./net_latency_summary.ts"; ← from net.latency-summary ^1.0.0 · built alongside by fune
import { type StatusResult, type StatusThresholds } from "./net_device_status_types.ts";
// Integer arithmetic only, so the reason text is identical in every language.
function percent(basisPoints: number): string {
return `${Math.floor(basisPoints / 100)}.${String(basisPoints % 100).padStart(2, "0")}%`;
}
function ms(us: number): string {
return `${Math.floor(us / 1000)}.${String(us % 1000).padStart(3, "0")} ms`;
}
/**
* Up, degraded or down, and every reason, from one latency summary.
*
* Down wins outright: once a device is down, its p95 and jitter are beside
* the point. Otherwise every threshold crossed is listed, loss first, so an
* alert says all that is wrong at once.
*/
export function deviceStatus(summary: LatencySummary, thresholds: StatusThresholds): StatusResult {
const { sent, received, lost } = summary;
if (sent < 1 || received < 0 || lost < 0 || received + lost !== sent) {
throw new RangeError(`summary is inconsistent: sent ${sent}, received ${received}, lost ${lost}`);
}
const { degradedLossBasisPoints: degLoss, downLossBasisPoints: downLoss, degradedP95Us: p95Limit, degradedJitterUs: jitterLimit } = thresholds;
if (!Number.isInteger(downLoss) || downLoss < 1 || downLoss > 10000) {
throw new RangeError(`downLossBasisPoints must be 1 to 10000, received ${downLoss}`);
}
if (!Number.isInteger(degLoss) || degLoss < 1 || degLoss > downLoss) {
throw new RangeError(`degradedLossBasisPoints must be 1 to downLossBasisPoints (${downLoss}), received ${degLoss}`);
}
if (!Number.isInteger(p95Limit) || p95Limit < 1) {
throw new RangeError(`degradedP95Us must be at least 1, received ${p95Limit}`);
}
if (jitterLimit !== null && jitterLimit !== undefined && (!Number.isInteger(jitterLimit) || jitterLimit < 1)) {
throw new RangeError(`degradedJitterUs must be null or at least 1, received ${jitterLimit}`);
}
if (received === 0) {
return { status: "down", reasons: [`no replies: ${lost} of ${sent} probes lost`] };
}
const loss = summary.lossBasisPoints;
if (loss >= downLoss) {
return { status: "down", reasons: [`loss ${percent(loss)} at or above the down threshold ${percent(downLoss)}`] };
}
const reasons: string[] = [];
if (loss >= degLoss) reasons.push(`loss ${percent(loss)} at or above ${percent(degLoss)}`);
if (summary.p95Us !== null && summary.p95Us >= p95Limit) {
reasons.push(`p95 ${ms(summary.p95Us)} at or above ${ms(p95Limit)}`);
}
if (jitterLimit !== null && jitterLimit !== undefined && summary.jitterUs !== null && summary.jitterUs >= jitterLimit) {
reasons.push(`jitter ${ms(summary.jitterUs)} at or above ${ms(jitterLimit)}`);
}
return { status: reasons.length > 0 ? "degraded" : "up", reasons };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add net.device-status
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./net.device-status-1.0.0-typescript.fune, or fetch it from a terminal with fune pull net.device-status@1.0.0:typescript.
The whole function, every language, is one file too: net.device-status-1.0.0.fune, 23,784 bytes, sha256 7d524303a5558a52cdbb5aa7c08d00a4663572dfad2cd19a7e4a2a98f96e7611. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before net.device-status
after — your function gets the result and the arguments, and returns the final result.
// fune: after net.device-status
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace net.latency-summary in net.device-status
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show net.device-status --steps.
// fune: step net.device-status after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a healthy link is up with no reasons | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… | → | status up, reasons |
| no replies at all is down | sent 3, received 0, lost 3, loss basis points 100%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 u… | → | status down, reasons no replies: 3 of 3 probes lost |
| loss above the down threshold is down | sent 3, received 1, lost 2, loss basis points 66.67%, min us 700, avg us 700, max us 700, p95 us 700, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degr… | → | status down, reasons loss 66.67% at or above the down threshold 50.00% |
| loss exactly at the down threshold is down | sent 4, received 2, lost 2, loss basis points 50%, min us 10, avg us 15, max us 20, p95 us 20, jitter us 10, degraded loss basis points 10%, down loss basis points 50%, degraded p… | → | status down, reasons loss 50.00% at or above the down threshold 50.00% |
| a quarter lost is degraded | sent 4, received 3, lost 1, loss basis points 25%, min us 1,000, avg us 2,000, max us 3,000, p95 us 3,000, jitter us 1,500, degraded loss basis points 10%, down loss basis points … | → | status degraded, reasons loss 25.00% at or above 10.00% |
| loss exactly at the degraded threshold is degraded | sent 10, received 9, lost 1, loss basis points 10%, min us 1,000, avg us 1,000, max us 1,000, p95 us 1,000, jitter us 0, degraded loss basis points 10%, down loss basis points 50%… | → | status degraded, reasons loss 10.00% at or above 10.00% |
| loss just under the degraded threshold is up | sent 11, received 10, lost 1, loss basis points 9.09%, min us 1,000, avg us 1,000, max us 1,000, p95 us 1,000, jitter us 0, degraded loss basis points 10%, down loss basis points … | → | status up, reasons |
| p95 exactly at the threshold is degraded | sent 4, received 4, lost 0, loss basis points 0%, min us 50,000, avg us 70,000, max us 100,000, p95 us 100,000, jitter us 10,000, degraded loss basis points 10%, down loss basis p… | → | status degraded, reasons p95 100.000 ms at or above 100.000 ms |
| loss, p95 and jitter reasons are all listed, in that order | sent 4, received 3, lost 1, loss basis points 25%, min us 90,000, avg us 120,000, max us 152,300, p95 us 152,300, jitter us 25,000, degraded loss basis points 10%, down loss basis… | → | status degraded, reasons loss 25.00% at or above 10.00%, p95 152.300 ms at or above 100.000 ms, jitter 25.000 ms at or above 20.000 ms |
| a null jitter threshold ignores jitter | sent 4, received 4, lost 0, loss basis points 0%, min us 1,000, avg us 30,000, max us 60,000, p95 us 60,000, jitter us 50,000, degraded loss basis points 10%, down loss basis poin… | → | status up, reasons |
Show the other 12 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a single reply has no jitter to judge | sent 1, received 1, lost 0, loss basis points 0%, min us 7, avg us 7, max us 7, p95 us 7, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 us … | → | status up, reasons |
| a 5 bp loss prints as 0.05% | sent 2,000, received 1,999, lost 1, loss basis points 0.05%, min us 100, avg us 100, max us 100, p95 us 100, jitter us 0, degraded loss basis points 0.05%, down loss basis points … | → | status degraded, reasons loss 0.05% at or above 0.05% |
| sub-millisecond latency prints with three places | sent 2, received 2, lost 0, loss basis points 0%, min us 7, avg us 7, max us 7, p95 us 7, jitter us 0, degraded loss basis points 10%, down loss basis points 50%, degraded p95 us … | → | status degraded, reasons p95 0.007 ms at or above 0.007 ms |
| no replies is down even when the down threshold is 100% | sent 1, received 0, lost 1, loss basis points 100%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 100%, degraded p95 … | → | status down, reasons no replies: 1 of 1 probes lost |
| received plus lost must equal sent | sent 3, received 2, lost 2, loss basis points 66.67%, min us 1, avg us 1, max us 1, p95 us 1, jitter us 0, degraded loss basis points 10%, down loss basis points 50%, degraded p95… | → | error: summary is inconsistent |
| a summary of no probes is an error | sent 0, received 0, lost 0, loss basis points 0%, min us —, avg us —, max us —, p95 us —, jitter us —, degraded loss basis points 10%, down loss basis points 50%, degraded p95 us … | → | error: summary is inconsistent |
| a zero down threshold is an error | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… | → | error: downLossBasisPoints must be 1 to 10000 |
| a down threshold over 100% is an error | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… | → | error: downLossBasisPoints must be 1 to 10000 |
| a degraded threshold above the down threshold is an error | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 60%, down loss basis poin… | → | error: degradedLossBasisPoints must be 1 to downLossBasisPoints |
| a zero degraded loss threshold is an error | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 0%, down loss basis point… | → | error: degradedLossBasisPoints must be 1 to downLossBasisPoints |
| a zero p95 threshold is an error | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… | → | error: degradedP95Us must be at least 1 |
| a zero jitter threshold is an error | sent 4, received 4, lost 0, loss basis points 0%, min us 10,000, avg us 11,500, max us 13,000, p95 us 13,000, jitter us 1,667, degraded loss basis points 10%, down loss basis poin… | → | error: degradedJitterUs must be null or at least 1 |
More from the author
1. No replies at all: **down**, `no replies: 3 of 3 probes lost`. 2. Loss at or above `downLossBasisPoints`: **down**, `loss 66.67% at or above the down threshold 50.00%`. Down wins outright; p95 and jitter are not judged. 3. Otherwise each threshold crossed adds a reason, in this order, and any reason makes it **degraded**: - loss at or above `degradedLossBasisPoints`: `loss 25.00% at or above 10.00%` - p95 at or above `degradedP95Us`: `p95 152.300 ms at or above 100.000 ms` - jitter at or above `degradedJitterUs` (skipped when the threshold or the jitter is null): `jitter 25.000 ms at or above 20.000 ms` 4. No reasons: **up**, with an empty list.
Every comparison is "at or above", so a threshold is the first value that counts as bad. Percentages are printed from basis points with two places and times from microseconds as milliseconds with three places, using integer arithmetic only, so the text is identical in every language.
## Errors
- `summary is inconsistent` when `sent` is under 1 or `received + lost` differs from `sent`. - `downLossBasisPoints must be 1 to 10000` - `degradedLossBasisPoints must be 1 to downLossBasisPoints` (a degraded threshold of 0 would make every device degraded) - `degradedP95Us must be at least 1` - `degradedJitterUs must be null or at least 1`
Files
| Path | Bytes |
|---|---|
| README.md | 1,556 |
| impl/python.py | 2,915 |
| impl/rust.rs | 3,980 |
| impl/typescript.ts | 2,811 |
| vectors.json | 8,602 |