# JSON results

`udpstp client --format json` prints the Test's results as JSON compatible with OB-UDPST v9.1.0's client output: the same keys, in the same order, with numbers and times printed the way OB-UDPST prints them, so tools written for OB-UDPST's formatted JSON (`-f jsonf`) keep working. Progress goes to stderr, so stdout holds only the JSON.

From 0.1.0 the key names, types and enumerated values are a [stable interface](../reference/interfaces.md), the extension's included; the free text in `ErrorMessage`, `ErrorMessage2` and a warning's `Message` is not.

## What is the same

- Top-level keys: `IPLayerMaxConnections`, `IPLayerMaxIncrementalResult`, `IPLayerCapSupported`, then `Input` and `Output` once the Test was activated, then `ErrorStatus`, `ErrorMessage` and `ErrorMessage2`.
- `Output`: `BOMTime` to `TimestampResolutionUsed`, then `IncrementalResult`, `Summary`, `ModalSummary`, `AtMax`, `ModalResult`, `ECNBleachingDetected`, then `EOMTime` and `Status`.
- Printed forms: delays in seconds from whole milliseconds (`0.011`), Mbps and percentages with 2 decimals, ratios with 9, zero as `0.0`, times as UTC with microseconds (`2026-09-25T14:13:20.712759Z`), and OB-UDPST's tab-indented layout.
- A Test that never activated has only the top-level `IPLayer*` keys and the error fields; one that didn't complete (ErrorStatus 200 and up) has only `Output`'s header and trailer.

Tests compare the key paths, their order and each value's printed form with OB-UDPST's vendored examples 01, 06, 11 and 12 (`testdata/obudpst-9.1.0/json_examples`).

## Fields udpstp never measures

They carry OB-UDPST's own value for the feature being off:

| Value | Fields |
|---|---|
| `0` or `0.0` | `CECountOfDelivered`, `CEPercentOfDelivered` (every object), `ECNBleachingDetected`, `Input.ECN`, `Input.CEThreshold` |
| `""`, `0.0` or `0` | `Input.Interface`, `InterfaceEthMbps` (every object), `Input.InterfaceDeterminesMax` |
| fixed | `NumberFirstModeSuppSubIntervals` 0, `PreambleDuration` 0, `IPRREnable` 1, `RIPREnable` 1, `TimestampResolution` and `TimestampResolutionUsed` 1, `Tmax`, `TmaxUsed`, `TimeoutNoTestTraffic` and `TimeoutNoStatusMessage` 1000 (OB-UDPST's 1 s traffic-stopped timer, not RFC 9097's Tmax), `TmaxRTT` and `TmaxRTTUsed` 3000, `ProtocolVersion` `"Any"`; `JumboFramesPermitted`, `UDPPayloadMax` and `UDPPayloadDefault` follow `--jumbo` and `--traditional-mtu` |
| Multi-Connection | `NumberOfConnections` and `MinNumOfConnections` from `--test-connections` and `--min-test-connections`, `NumberOfHosts` and `HostList` from the SERVERs; a Downstream Test's Sub-Intervals are its Test Connections' summed on one grid, where OB-UDPST sums them by index, so the figures can differ from OB-UDPST's; `ActiveConnections` counts the Test Connections that received in the Sub-Interval (1 with one Test Connection, as OB-UDPST), in a Summary in its Capacity Mode's last; `MinOnewayDelay` is 0 when the Test Connections go to several Servers, whose clocks the Client can't relate. An Upstream Test's Sub-Intervals are each Server's own, summed by number, so they are approximate: a warning in the `udpstp` extension, not ErrorMessage, gives the largest offset between them |

`ClientIPAddress` is `""` today: the Client's socket is bound to a wildcard address, which says nothing about the one used. When it holds an address, it follows `--address-privacy` (below).

## Where udpstp differs

Each is an [OB-UDPST Difference](../conformance/format.md) with its own Conformance Record.

- **`AtMax` and `ModalResult` are the Maximum IP-Layer Capacity**, which meets the Performance Criterion. OB-UDPST's maximum, the Highest IP-Layer Capacity, is in the extension. A Capacity Mode with no maximum has no `AtMax` (or `ModalResult`), never zeros, and the Test gets ErrorStatus 49.
- **ErrorStatus** is the most severe status, where OB-UDPST keeps the last one written: 200 for a Test that didn't complete, with its cause in `ErrorMessage2`; for a completed Test the most severe warning: 49 no valid maximum, 4 nothing received for 1 s, 5 the peer stopped receiving, 3 the peer missed Status PDUs, 2 Status PDUs missing here, 6 invalid PDUs. The exit status of a completed Test is 0 all the same.
- **An interrupted Test** (Ctrl-C, a signal to stop, or a failed write to standard output while the Test ran) has ErrorStatus 204 and the results so far, its Test Connections having sent their STOP; OB-UDPST stops without one and exits 255.
- **A Test that didn't complete** keeps its measured Sub-Intervals in `udpstp.SubIntervals`, with the same fields and units as `Output.IncrementalResult`, and its minimum RTT in `udpstp.MinRTT`. `Output` still has only its header and trailer. These partial results require the extension, which is on by default.
- **`SoftwareVersion`** is `"udpstp <version>"`; `ControlProtocolVersion` is 20.
- **Delay fields of a Sub-Interval without samples** are 0, as OB-UDPST writes, and the extension lists those Sub-Intervals; `Summary` is computed from real samples only.
- **A Sub-Interval never measured** is left out of `IncrementalResult` and listed in the extension; `Interval` is its subIntSeqNo.
- **`IPLayerCapacity`** is over the nominal Sub-Interval (RFC 9097 §5.3); OB-UDPST divides by the measured one.
- **A name that doesn't resolve, or a Client that can't start**, prints no JSON; OB-UDPST prints JSON with 255.

## The extension

One last object, `"udpstp"`, holds results OB-UDPST's schema lacks; `--json-extension=false` leaves it out for tools that reject unknown keys:

| Key | What |
|---|---|
| `Warnings` | The results' warnings, each `{Code, Count, Message}`: the text results' line as `Message`, which may change; `Code`, the Server log's event name for it (such as `BurstsSkipped`, `PathMtuExceeded`, `TrafficStopped`, `SocketBuffersShort`, or `DataPhaseEnded` for a Test that ended without the STOP exchange: in a Multi-Connection Test, one for each Test Connection that did, its `Message` naming it), or `SendingRateCapped`, `FirstModeSplitIgnored`, `UpstreamResultsApproximate`, `Interrupted` (the Client user stopped the Test), `ReceiveBufferOverflows` (datagrams this host's OS discarded at the socket before udpstp read them; Linux only), `SendingRateNotKept` (a Downstream Fixed-Rate Test whose Server sent more than 5% below its row's sending rate in the Sub-Interval of a Maximum IP-Layer Capacity, as the Client estimated what it sent: it may not have kept up, so the result may understate the path; its `Count` is the Capacity Modes it concerns; a Search Test's Server tells the Client no sending rate), `CongestionNeverConfirmed` (a Search Test none of whose Test Connections' searches found the path's limit, as the Client classified their Trial Intervals, by the maximum delay variation: none confirmed Congestion, slowAdjThresh Errored Intervals back to back, nor met the limit at its end, at least 4 of its last 64 Trial Intervals errored and at least 1 in 4 from the first of those on, as a search cycling around the limit above the 1 Gbps row does, so the result may understate the path: one for each Test Connection, in a Multi-Connection Test its `Message` naming it, saying why its search fell short, the first that fits ([its limits](../understanding-results.md#when-the-search-never-confirmed-congestion)): its IP-Layer Sender Bit Rate was at the maxBandwidth it stated, or reached the top of the Sending Rate Table; delay variation held it in Hold Intervals; the Test ended while it still rose; or else the Load Sender, the Server in a Downstream Test and this host in an Upstream one, stopped rising though few of the last 64 Trial Intervals were Errored Intervals, so it may not have kept up. Only for a completed Search Test whose every activated Test Connection completed), `SubIntervalsAfterMaximumMissed` (a Search Test in which no Sub-Interval after the Maximum IP-Layer Capacity's met the Performance Criterion, and one that missed it ran more than `--faster-sub-interval-margin` faster than the Maximum, after the Maximum's, as a search cycling around a drop-tail bottleneck gives, or before it, as a search that climbed back slowly after an overshoot, a path that slowed, a policer or shaper that let a burst through at first or, Downstream, a Server that fell behind gives; or the last one ran under 60% of the Maximum, as a queue that kept dropping after it overflowed, a path that slowed or, Downstream, a Server that fell behind gives, so the Maximum may be well below the path, or no longer what it carries: one for each Capacity Mode, its `Message` giving each such Sub-Interval, whether before or after the Maximum's, with its IP-Layer Capacity and Loss Ratio; an Upstream Test in which this host left more than 1% of its Load PDUs unsent isn't judged on the last one or on one before the Maximum's) or `NoValidMaximum`; and `Count`, the datagrams or PDUs it counts, or 1. `NoValidMaximum` is listed only here |
| `EndReason` | How the Test ended. With one Test Connection, how it ended: `Completed`, `Unconfirmed`, `TestIntervalElapsed`, `StoppedByClient`, `Stopped`, `Watchdog` or `OverRate`, or `DataPhaseEngineFailed`, `ActivationRejected`, `ControlPhaseFailed` (a time limit too), `Abandoned` (given up by the Multi-Connection Coordinator), `SocketError`. A Multi-Connection Test's is the Test's own: `Completed`, or why the Multi-Connection Coordinator aborted it, `TooFewTestConnections`, `ParametersDiffer`, `TestConnectionEnded` (one refused a Coercion or can't run a parameter), `TestConnectionsTimedOut` or `Interrupted`, or `NoTestConnectionCompleted` when every Test Connection ended otherwise; each Test Connection's own end is in `TestConnections` |
| `ControlPhaseFailure` | Why the Control Phase failed, when `EndReason` is `ControlPhaseFailed`, else `null` (so always `null` for a Multi-Connection Test, whose Test Connections each have their own in `TestConnections`): `TestInitiationTimedOut`, `NoAuthenticationConfigured`, `NoKeyForSending`, `AmbiguousKeyForSending`, `UnacceptableCoercion`, `UnrunnableParameter`, `UnknownSetupResponse`, `UnknownActivationResponse`, `MulticastOrBroadcastServer`, `SocketUnavailable`, or `SetupRejected` and the Setup Response's code: `SetupRejectedBadVersion`, `SetupRejectedJumboMismatch`, `SetupRejectedTraditionalMtuMismatch`, `SetupRejectedAuthNotConfigured`, `SetupRejectedAuthRequired`, `SetupRejectedAuthModeInvalid`, `SetupRejectedAuthFailed`, `SetupRejectedAuthTimeInvalid`, `SetupRejectedMaxBandwidthRequired`, `SetupRejectedCapacityExceeded`, `SetupRejectedMultiConnectionInvalid`, `SetupRejectedConnectionAllocationFailure` |
| `Coercions` | Each parameter the Server coerced, `{McIndex, Parameter, Requested, Coerced}`: the Test Connection, the parameter's RFC 9946 name as `--refuse-coercion` takes it, and the values as the Activation Request and Response carry them (`dscpEcn` whole, the DSCP shifted left by 2) |
| `DontFragment`, `SocketBuffers`, `DataPhaseEngine` | How this host ran the Test. `DataPhaseEngine` is `portable`. In a Multi-Connection Test, `SocketBuffers` is the smallest the OS granted any of its Test Connections, receive and send each |
| `FastPaths` | The fast paths this host sent Load PDUs, read datagrams and waited with, in this order: `sendmmsg` (Load PDUs sent in batches), `UdpGso` (a Burst's Load PDUs as one message the kernel splits), `recvmmsg` (datagrams read in batches, Load PDUs header-only), `KernelRxTimestamps` (datagrams timed by the kernel's receive timestamps rather than the read) and `ppoll` (the Data Phase Engine waited on its socket between turns, so an arrival woke it, rather than sleeping). Linux only; empty elsewhere. `sendmmsg` and `UdpGso` are missing in a Downstream Test (the Client sends no Load PDUs) and where the kernel refused `sendmmsg`; `UdpGso` also where the kernel or device refused it, or no Burst held more than one Load PDU. With `UDPSTP_KERNEL_TX_TIMESTAMPS=1` on Linux 6.13 and later `sendmmsg` is missing too, since each message then goes with a `sendmsg` call of its own to carry the number of its send timestamp, so `UdpGso` can come without it; if the kernel refuses the numbers after all, the batches after that go in `sendmmsg` calls, and it is named. With `UDPSTP_KERNEL_TX_TIMESTAMPS=kernel-count` the messages carry no numbers, and `sendmmsg` is named as without the variable. `recvmmsg`, `KernelRxTimestamps` and `ppoll` are missing where the kernel refused them. In a Multi-Connection Test, the fast paths every Test Connection that completed used, so a fallback on any one shows; if none completed, every one whose Data Phase Engine ran until its Data Phase ended. A Test Connection whose engine failed or never ran, or that ended early, is left out: it used none, or not all it would have |
| `KernelSendTimestamps` | Only with `UDPSTP_KERNEL_TX_TIMESTAMPS` set to `1` or `kernel-count` on Linux, in a Test where the Client is the Load Sender: how much later than their lpduTime the kernel sent the Load PDUs, from its software send timestamps, one per message (a UDP GSO message counts once): `Samples`, and `LagP50Us`, `LagP99Us` and `LagMaxUs` in µs. On Linux 6.13 and later each message carries its own number, so a send the kernel refuses costs only its own sample; each message then goes with a `sendmsg` call of its own, and `FastPaths` doesn't name `sendmmsg`. A kernel that refuses the numbers after all (EINVAL) gets its messages without them from then on, its batches in `sendmmsg` calls, which `FastPaths` then names, and counting stops there, the Test going on. Before 6.13 the kernel numbers the messages, and may have numbered one it refused (a full queue, a refused UDP GSO send), so counting stops at the first send refused, whole or in part, and a Test with refused sends reports fewer samples. With `kernel-count` the kernel numbers them on every kernel, as before 6.13, so a result can be checked on that send path: batches in `sendmmsg` calls, counting stopping at the first send refused. In a Multi-Connection Test, its Test Connections' samples counted together, with the largest of their lags. Experimental: not a stable interface ([interfaces.md](../reference/interfaces.md)) |
| `BurstLateness` | In a Test where the Client is the Load Sender: how late after their scheduled time it wrote its Send Plans, and so stamped their lpduTime, its Burst lateness: `Samples`, one a Send Plan, and `P50Us`, `P99Us` and `MaxUs` in µs, counted in 1 µs buckets up to 10 ms. In a Multi-Connection Test, its Test Connections' samples counted together, with the largest of their percentiles. The Server's log reports its own as `burst_lateness_p99_us` in `DataPhaseEnded`. Experimental: not a stable interface ([interfaces.md](../reference/interfaces.md)) |
| `PerformanceCriterion` | `MaxLossRatio` |
| `CapacityModes` | Each Capacity Mode's Sub-Intervals, whether it has a valid result, its Maximum and Highest IP-Layer Capacity |
| `SubIntervals`, `MinRTT` | Only for a Test that activated but didn't complete, including an interrupted Test. `SubIntervals` holds every measured Sub-Interval with the same fields as `Output.IncrementalResult`, preserving its `Interval` number, loss, delay variation, RTT variation and IP-Layer Capacity. It is empty if none was measured. `MinRTT` is the smallest measured minimum RTT, in seconds, or `null` if unavailable. Neither field is a completed-Test summary |
| `MissingSubIntervals`, `NoSamples` | Sub-Intervals never measured, and those without delay or RTT samples |
| `IPLayerSenderBitRate` | Per Sub-Interval, where the Client was the Load Sender; in a Downstream Test `IPLayerSenderBitRateEstimate` instead, the Client's estimate of what the Server sent in each st of the Server's clock, from the lpduSeqNo span of the Load PDUs that arrived, a lost one as big as the st's others |
| `TestConnections` | A Multi-Connection Test's Test Connections, each on its own, as the text results show them: `{McIndex, Server, EndReason, ControlPhaseFailure, CapacityModes}`. A Test with one Test Connection has none |

## Not yet

- Downstream output follows OB-UDPST's code, where only `Role` changes; every OB-UDPST example is an Upstream Test, so a Downstream Test's full structure hasn't been compared with OB-UDPST's.
- OB-UDPST's brief (`jsonb`) and summary-only (`-s`) forms.

`ClientIPAddress` follows `--address-privacy`: the address in full (the default), `anon-` and a keyed hash of it (`pseudonymize`, with `--address-privacy-key-file`), or its IPv4 /24 or IPv6 /48 (`truncate`).
