# Latency method, sources and how to refill the numbers Owner: whoever publishes the latency figures on the site. The section that carried them on `site/compare.html` was removed on 17 October 2026, so `site/assets/latency.json` is now the only published copy of them. This file is the contract for that JSON: what a field means, which figures were measured, which were quoted from the specification and client code, and the exact command that regenerates the measured ones. The comparison against other products was **withdrawn on 16 October 2026**. An earlier version of the page charted Veiled Contact's latency beside Discord's. The deciding term on Discord's side is a receive buffer they do not publish, which made the comparison an argument rather than a measurement, so the chart and the competitor figures came off the site. What follows is the method for Veiled's own figures, which are all that remain in the file. Last updated: 16 October 2026. ## 1. What the figures claim, and what they refuse to claim They show one way voice latency split into the three parts that decide it: 1. **Codec frame.** The grid a sender has to fill before a frame can leave. 2. **Receive jitter buffer.** The audio a listener holds to absorb reordering and jitter. 3. **Network leg.** The path to whatever relay the product uses. It is a sum of parts, not a listening test. It excludes microphone capture, the operating system audio stack, codec lookahead and loudspeaker playback, all of which are the same for every product here and none of which were measured. The page refuses to print a number it cannot point at. Every figure carries a state, and the page labels each row with the state it is in: | State in the JSON | Meaning | | --- | --- | | `measured` | Produced by a command on the measurement host, or by `veiled-bench`. The command is recorded in `measurements.entries`, and the page says which machine and which date. | | `derived` | Quoted from Veiled's frozen specification or its client code, with the quote in `sources`. Not measured here, and the page says so rather than presenting it as a measurement. | | `not_published` | No figure exists that can be cited or measured. Only used for a competitor's figure, and retained here for a file that might chart one again. | | `pending` | A measurement slot that exists and has not been filled. | The rule: a value is a number only when its `kind` is `measured` or `derived` and the numeric fields are finite. Anything else stays words. The full audio chain, a voice in at one end and out at the other, is `pending`: the relay's own share is measured, and the frame and the buffer are documented rather than measured, so no total is printed. ## 2. Schema of `site/assets/latency.json` ``` schema_version integer, bump on any breaking change method path to this document generated date the file was produced, ISO 8601, date only note one paragraph stating the no-number-without-a-state rule kinds a definition per state, for whoever edits the file next measurements host description, os, kernel, address, gateway, location, network date date the commands ran entries one object per measured figure: label, command (verbatim, copy pastable), unit, samples, loss_pct, min_ms, mean_ms, max_ms, mdev_ms, optional p50_ms, optional caveat explaining what the number is not products[] one per product measured here, which is Veiled Contact id, name, tag id is used by the index strip and by the renderer headline one sentence, for a human reading the file segments[] frame, buffer, network, in that order, stacked left to right component frame | buffer | network label row label in tables kind measured | derived | not_published | pending low_ms/high_ms present only when kind is measured or derived; a range is low to high ceiling_ms optional, the widest the value can grow under load display the words the page prints for this segment region_note optional, the words printed across a not_published region why one or two sentences a reader can check measurement the key in measurements.entries this value came from reason why there is no number: vendor_publishes_no_figure | not_measured_here sources[] ids resolved against sources[] total kind, low_ms, high_ms, display, note, pending_measurement display_short a short form of the total, for anywhere with no room for the long one bench only on Veiled: state plus the veiled-bench fields, see section 4 pending_measurement state pending while the full audio chain has not been measured rule the no-number-without-a-state rule, in words what_is_pending sources[] id, org, title, url, accessed, quote ``` Two conventions matter more than the rest: - A range is printed as a range, low to high, and never collapsed into a single number that looks like a point estimate. - Nothing is summed into a total unless every part of the sum is measured. A documented part and a measured part are different kinds of claim, and adding them produces a third thing that is neither. ## 3. What was measured, and what was dropped ### Measured on this host (16 October 2026) Host: a Proxmox container, Ubuntu 24.04.4 LTS, kernel `7.0.14-15-pve`, address `192.168.1.205`, gateway `192.168.1.1`, Datchet, United Kingdom, ordinary home broadband. | Figure | Command | Result | | --- | --- | --- | | Local gateway, standing in for a Veiled server next to the listeners | `ping -c 20 -i 0.2 192.168.1.1` | min 0.278, mean 0.354, max 0.442, mdev 0.048 ms, twenty samples, no loss | | Veiled relay, media one way, through TLS, the password proof, the database and the relay | `veiled-bench --server wss://127.0.0.1:9443/ws --clients 3 --duration 20 --rate 100 --loss 0.02 --json bench.json` | p50 0.180, p95 0.298, mean 0.199 ms over 1,958 frames | | Veiled relay, control round trip | the same run | p50 0.145, p95 0.260 ms over 1,200 round trips | The network figure is an ICMP figure on a path carrying no audio, so it stands in for a server next to the people talking rather than measuring a call. The relay figures are on loopback, which makes them the floor of that path rather than a typical internet figure. The three measurements of Discord's network legs that used to sit in this table were removed with the comparison on 16 October 2026, along with the note that their per call voice endpoints are handed out per session and are not published. The figures are in the git history if anyone wants to argue about them again, and the reason they are gone is in section 1. ### Quoted from documentation | Figure | Value | Source id | | --- | --- | --- | | Veiled codec frame | 10 ms Opus frames in a 28 byte envelope | `veiled-protocol`, `opus-rfc` | | Veiled receive buffer | 10 to 20 ms adaptive, ceiling 12 frames (120 ms) | `veiled-latency-budget`, `veiled-client-media` | | Veiled server behaviour | forwards verbatim, no transcode, no mix, no server jitter buffer | `veiled-architecture` | ### Not measured, and the gap is shown | Figure | Why there is no number | Where a figure was looked for | | --- | --- | --- | | The full Veiled Contact audio chain | Not measured yet. It needs two clients in one call, a known signal injected at one end and recorded at the other, and one clock for both ends | n/a | | Any competitor figure | The comparison was withdrawn: the deciding term on the other side is unpublished, so no honest side by side exists | See section 1 | A community wiki, a forum post or a screenshot is not a source for a number here. If a figure cannot be pointed at in the specification or in a command that was run, it is left out and the gap is shown. ## 4. Refilling `latency.json` from a real bench run `veiled-bench` is the crate in `tools/bench`. It writes a JSON summary whose field names are the ones `latency.json` already uses, so nothing has to be renamed. ```sh # 1. Build the harness (from the repository root). cargo build --release -p veiled-bench # 2. Check the harness before trusting it. This starts its own endpoint, # runs the measurements and exits non-zero if a check fails. ./target/release/veiled-bench --self-test # 3. Measure a running server: one run against the loopback echo baseline, # one against your server. Add --insecure for a self signed certificate, # --join-password if the server has one set. ./target/release/veiled-bench \ --server wss://your-host:8443/ws \ --clients 3 --duration 30 --rate 50 --loss 0.02 \ --json /tmp/bench.json # 4. See what it produced. python3 -m json.tool /tmp/bench.json | head -40 ``` Then edit `site/assets/latency.json`, in `products[veiled].bench`: - Set `state` to `"measured"` at the top level of `bench` only once every field below is filled. - `generated_unix_s`, `proto`, `tool_version`: copy from the bench document. - `handshake_ms`, `control_rtt_ms`, `media_owd_ms`: copy `runs[0].handshake_ms`, `runs[0].control_rtt_ms` and `runs[0].media_owd_ms` from `/tmp/bench.json`, and add `"state": "measured"` to each object. Keep the raw field names: `count`, `min_ms`, `p50_ms`, `p95_ms`, `p99_ms`, `max_ms`, `mean_ms`. A value with any other state, or with a field that is not a finite number, must not be printed as a number. - `media` and `measured`: copy `runs[0].media` and `runs[0].measured` for the record. - Update `measurements.entries` with the host used for this run, and set `pending_measurement.state` to `"measured"` only when nothing is left pending. - Update the `generated` date, and the dated line in the method note on `compare.html`. Rules that apply to any edit: - Never put a number in a `not_published` segment. Set `reason` and `display` instead. - Never estimate a competitor figure. If the vendor does not publish it, it stays unpublished, even when the number is obvious to everyone. That rule is what withdrew the comparison chart. - When you add a vendor figure, add the URL and the quote to `sources` first, then reference it by id from the segment, and keep the wording in `site/compare.html` consistent with it. - A measured value must carry the exact command in `measurements.entries`, so a reader can rerun it. - After editing, check the page and the file over HTTP: load `/assets/latency.json` through the server and confirm it still parses, then confirm the figures on the page match it (`curl -s http://127.0.0.1:8080/compare.html | grep -o '0\.18 ms'`). ## 6. The measurement that is still owed, and how to take it The full audio chain has not been measured: a voice in at one end of a real call and out at the other. The rig below was built and validated on 16 October 2026 and the run was not made. This is what it needs to be, so nobody rebuilds it from scratch. **The signal.** A click train, 2 ms bursts of 2 kHz with a hard onset, every 250 ms. Written with the standard library, no dependencies. Short bursts correlate to sample accuracy; a long tone does not. **The rig, on one machine so both ends share a clock.** Two null sinks. `vt-mix` receives the receiving client's output and is recorded (`pw-record --target vt-mix.monitor`). The click train is played into `vt-mix` as well, so **the recording contains the injected copy and the copy that came back through the product**. The injected copy is its own timestamp reference, which means the measurement never depends on knowing when playback started and never depends on two clocks agreeing. This is the part worth keeping: a latency figure that rests on clock sync is a figure nobody can check. **Why the injection is not a second playback.** Two `pw-play` processes started back to back differ by process startup, which is milliseconds, which is the size of the thing being measured. Play the train once, into the sink that is both recorded and injected, and the skew problem does not exist. **Two seats, and the trap.** The product needs a distinct participant at each end. Discord will not allow it: **one account can hold only one voice session per channel, so the second join kicks the first.** Two sessions of one account, two instances of one client, or the same client on two machines all fail the same way. The second seat must be a second identity: a bot for the sending side, or a second account for both sides being real clients. **The seats themselves.** - Receiving seat: the product's own client, output routed to `vt-mix`. If it is a user client, route its audio with `pactl move-sink-input vt-mix` rather than driving the client's interface: the measurement should not require automating somebody's account. - Sending seat: microphone routed from the injection sink with `pactl move-source-output`. **What will make the number wrong if it is not handled.** Noise suppression, echo cancellation and automatic gain control, where the product has them, will chew up a signal that is not speech. Record what was enabled when the figure was taken and report both, or take the figure twice. A figure whose audio settings are unstated is not reproducible. **Both products, same rig.** Run the identical train, the identical sinks and the identical analysis for each, and report the difference between the injected copy and the returned copy. Do not halve a round trip, and do not subtract a documented figure from a measured one to manufacture a number that can be compared with it. ## 5. Sources, with URLs and quotes Read on 16 October 2026. Quoted text is verbatim from the document at that URL. **Veiled** - Wire protocol v1, section 4 (media plane). https://veiled.contact/docs/protocol.md "For every accepted media frame the server forwards the frame (verbatim, header included) to every other bound member of the same channel" - Architecture, media plane and the relay argument, and the latency budget table. https://veiled.contact/docs/ARCHITECTURE.md "It never decrypts, never decodes, never mixes, never transcodes and never stores." and "Voice, end to end | RTT + ~25 ms (10 ms frame + 10 to 20 ms jitter buffer)" - Client media plane, `FRAME_MS = 10`, jitter buffer depth from 1 to 12 frames. the client media plane, in the source tarball offered on the download page **Opus** - RFC 6716, Definition of the Opus Audio Codec, section 2.1.4 Frame Duration. https://www.rfc-editor.org/rfc/rfc6716#section-2.1.4 "Opus can encode frames of 2.5, 5, 10, 20, 40, or 60 ms" **Discord** (cited by the head to head table on the compare page, not by any latency figure) - Support, Voice Regions on Discord FAQ: calls are hosted by Discord, in regions. https://support.discord.com/hc/en-us/articles/1500007645701-Voice-Regions-on-Discord-FAQ "Discord uses a private voice hosting system where we host voice calls for our users for free." - Developer documentation, Voice Connections, DAVE section: end to end encrypted calls. https://docs.discord.com/developers/topics/voice-connections#end-to-end-encryption-dave-protocol "we will only support E2EE calls starting on March 1st, 2026 for all audio and video conversations in direct messages (DMs), group messages (GDMs), voice channels, and Go Live streams" - Privacy Policy: messages are collected and stored. https://discord.com/privacy "We similarly collect and store the messages you send and display them as you direct." - Terms of Service, accounts. https://discord.com/terms "To access the services on an ongoing basis, you will need to create a Discord account. You can provide a username and password, and a way of contacting you (such as an email address and/or phone number)" - Support, Audio Bitrate FAQ: bitrate caps and boosts. https://support.discord.com/hc/en-us/articles/11635925354775-Audio-Bitrate-FAQ "Level 1 - 2 Server Boosts = 128 Kbps Audio Quality"