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
device_status(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 reasonsdevice_status(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 downdevice_status(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.
def device_status(summary: LatencySummary, thresholds: StatusThresholds) -> StatusResult
| summary | LatencySummary | |
| thresholds | StatusThresholds | |
| returns | StatusResult |
The types it declares, generated into your project
@dataclass(frozen=True)
class StatusThresholds:
"""When a device stops counting as healthy."""
#: loss at or above this is degraded, at least 1
degraded_loss_basis_points: int
#: loss at or above this is down, up to 10000
down_loss_basis_points: int
#: p95 at or above this many microseconds is degraded
degraded_p95_us: int
#: jitter at or above this is degraded; null to ignore jitter
degraded_jitter_us: Optional[int]
DeviceStatus = Literal["up", "degraded", "down"]
@dataclass(frozen=True)
class StatusResult:
"""The status and every reason for it, in a fixed order."""
status: DeviceStatus
#: empty when up
reasons: List[str]
Your code names it in one line, in the file that uses it
from fune.net.device_status import device_status # 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.
from typing import List
from .net_latency_summary import LatencySummary ← from net.latency-summary ^1.0.0 · built alongside by fune
from .net_device_status_types import StatusResult, StatusThresholds
# Integer arithmetic only, so the reason text is identical in every language.
def _percent(basis_points: int) -> str:
return "%d.%02d%%" % (basis_points // 100, basis_points % 100)
def _ms(us: int) -> str:
return "%d.%03d ms" % (us // 1000, us % 1000)
def _whole(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def device_status(summary: LatencySummary, thresholds: StatusThresholds) -> StatusResult:
"""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.
"""
sent, received, lost = summary.sent, summary.received, summary.lost
if sent < 1 or received < 0 or lost < 0 or received + lost != sent:
raise ValueError("summary is inconsistent: sent %d, received %d, lost %d" % (sent, received, lost))
deg_loss = thresholds.degraded_loss_basis_points
down_loss = thresholds.down_loss_basis_points
p95_limit = thresholds.degraded_p95_us
jitter_limit = thresholds.degraded_jitter_us
if not _whole(down_loss) or down_loss < 1 or down_loss > 10000:
raise ValueError("downLossBasisPoints must be 1 to 10000, received %r" % (down_loss,))
if not _whole(deg_loss) or deg_loss < 1 or deg_loss > down_loss:
raise ValueError("degradedLossBasisPoints must be 1 to downLossBasisPoints (%d), received %r" % (down_loss, deg_loss))
if not _whole(p95_limit) or p95_limit < 1:
raise ValueError("degradedP95Us must be at least 1, received %r" % (p95_limit,))
if jitter_limit is not None and (not _whole(jitter_limit) or jitter_limit < 1):
raise ValueError("degradedJitterUs must be null or at least 1, received %r" % (jitter_limit,))
if received == 0:
return StatusResult(status="down", reasons=["no replies: %d of %d probes lost" % (lost, sent)])
loss = summary.loss_basis_points
if loss >= down_loss:
return StatusResult(status="down", reasons=["loss %s at or above the down threshold %s" % (_percent(loss), _percent(down_loss))])
reasons: List[str] = []
if loss >= deg_loss:
reasons.append("loss %s at or above %s" % (_percent(loss), _percent(deg_loss)))
if summary.p95_us is not None and summary.p95_us >= p95_limit:
reasons.append("p95 %s at or above %s" % (_ms(summary.p95_us), _ms(p95_limit)))
if jitter_limit is not None and summary.jitter_us is not None and summary.jitter_us >= jitter_limit:
reasons.append("jitter %s at or above %s" % (_ms(summary.jitter_us), _ms(jitter_limit)))
return StatusResult(status="degraded" if reasons else "up", reasons=reasons)Install
fune build
With that line in your source, in a Python project (language python in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the Python 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 Python implementation. Install it without the registry with fune add ./net.device-status-1.0.0-python.fune, or fetch it from a terminal with fune pull net.device-status@1.0.0:python.
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 |