Reading an OONI measurement¶
A single OONI measurement carries more than twenty top-level fields, with another twenty-odd inside test_keys. ASN observation data retrieval and analysis covers how to pull this public data; this page covers how to read it once retrieved.
Two real measurements from Taiwan are compared side by side below to set out how a Web Connectivity (web_connectivity) measurement is put together, and which upstream ooni/spec document defines each field.
Where these examples come from
Both are public measurements produced by OONI Probe in Taiwan on 2026-08-04. You can look up the raw content on OONI Explorer.
| Passed | Flagged as anomalous | |
|---|---|---|
| Target | http://presidentlee.tw/ |
https://ntc.party/ |
measurement_uid |
20260804085935.603513_TW_webconnectivity_4a5fd27dec0b32f6 |
20260804084548.416537_TW_webconnectivity_f4e7b0ab3d0251bf |
Three layers in one measurement¶
A measurement divides into three layers:
- The envelope: who measured, from where, with which software, and when. Every nettest shares this same set of fields, defined in df-000-base.
- The verdict: the conclusion the measurement reached. These fields vary by nettest and all live under
test_keys. Forweb_connectivitythey are defined in ts-017. - The evidence: the raw record behind the conclusion, covering every DNS query, TCP connection, TLS handshake and HTTP request, plus the control's equivalent records. Also under
test_keys, each with its owndf-specification.
The verdict layer states the conclusion; the evidence layer holds the records supporting it.
Retrieving a measurement¶
The public API returns a single measurement directly, with no local environment required. Start by listing measurements that match your criteria:
curl -s "https://api.ooni.io/api/v1/measurements?probe_cc=TW&test_name=web_connectivity&limit=5" \
| python3 -m json.tool | head -40
The measurement_uid in the response gets you the full content:
curl -s "https://api.ooni.io/api/v1/raw_measurement?measurement_uid=<measurement_uid>" \
| python3 -m json.tool | head -60
The raw response is one long unbroken string, which is painful to read in a terminal, so both commands above pipe it through python3 -m json.tool. Adding anomaly=true returns only measurements flagged as anomalous, which is handy for finding contrasting examples. Swap probe_cc for your own region and probe_asn for your own network to see what has been observed locally.
For bulk processing, the AWS S3 public dataset is far more efficient. See ASN observation data retrieval and analysis.
The envelope: who measured from where¶
Envelope fields are identical across every nettest. These are the ones you reach for most often:
| Field | Passed | Flagged as anomalous | Meaning |
|---|---|---|---|
probe_cc |
TW |
TW |
Country code where the measurement happened |
probe_asn |
AS3462 |
AS3462 |
ASN of the network running the measurement |
probe_network_name |
Chunghwa Telecom Co., Ltd. |
Chunghwa Telecom Co., Ltd. |
Organisation name for that ASN |
resolver_asn |
AS3462 |
AS13335 |
ASN of the DNS resolver actually used |
resolver_network_name |
Chunghwa Telecom Co., Ltd. |
Cloudflare Inc |
Organisation name for the resolver |
input |
http://presidentlee.tw/ |
https://ntc.party/ |
URL being measured |
test_name |
web_connectivity |
web_connectivity |
Nettest name |
software_name |
ooniprobe-cli |
ooniprobe-desktop-unattended |
Which kind of Probe produced the data |
software_version |
3.29.1 |
3.26.0 |
Probe version |
measurement_start_time |
2026-08-04 08:59:30 |
2026-08-04 08:45:45 |
Start time in UTC |
report_id |
20260804T062033Z_webconnectivity_TW_3462_n4_uEH5rGoD07cN2oYQ |
20260804T084446Z_webconnectivity_TW_3462_n4_dFfWCDrwouM0TsT2 |
Shared by every measurement from the same run |
probe_asn and resolver_asn can differ, as the table shows. Both measurements ran on Chunghwa Telecom's network, but one of the users had pointed DNS at Cloudflare. Keep the two apart in ASN analysis, because conflating them distorts any claim about what is visible on a given carrier's network.
probe_ip is always 127.0.0.1
OONI deliberately does not collect the measurer's real IP, so probe_ip is hardcoded to the loopback address. Tracing a measurement back to its source relies on probe_asn plus timing. The same privacy boundary is why OONI Run v2 for regional measurement asks people to think about what they are asking helpers to run.
The verdict: what the measurement concluded¶
Verdict fields all live under test_keys. One thing to know before reading them: web_connectivity reaches its verdict by comparison. After the Probe finishes, an OONI test helper on an unrestricted network measures the same URL again, and the difference between the two sides is what the verdict is built from. The test helper's side is recorded under test_keys.control, which is what "the control" refers to below.
Eight fields carry the verdict and the content comparison, shown side by side below:
| Field | Passed | Flagged as anomalous | Meaning |
|---|---|---|---|
blocking |
false |
"dns" |
Type of interference found. Possible values are dns, tcp_ip, http-failure and http-diff, or the boolean false when nothing was observed |
accessible |
true |
false |
Whether a sensible response was obtained |
dns_consistency |
"consistent" |
"inconsistent" |
Whether the Probe's DNS result agrees with the control |
title_match |
true |
null |
Whether the page title matches the control |
headers_match |
true |
null |
Whether response headers match |
status_code_match |
true |
null |
Whether the HTTP status code matches |
body_length_match |
true |
null |
Whether body lengths are close |
body_proportion |
1 |
0 |
Body length as a ratio against the control |
Three more fields record where things failed, and belong in the same reading:
| Field | What it records |
|---|---|
dns_experiment_failure |
Failure during the DNS stage. "dns_nxdomain_error" in the anomalous measurement |
http_experiment_failure |
Failure during the HTTP stage, for example "generic_timeout_error" |
control_failure |
Whether the control itself failed. When this has a value, the comparison has no basis and the verdict cannot be trusted |
The four values of blocking map to failures at different stages. dns means the resolution disagreed with the control, tcp_ip means packets never reached the target address, http-failure means the connection was established but the HTTP stage failed, and http-diff means the content retrieved differs from the control, which commonly indicates a block page. OONI's own glossary introduces these interference techniques at a conceptual level.
Two type traps worth knowing about
blocking is the boolean false when no interference was observed, and a string when there was. Normalise it before running statistics, or false and "dns" end up counted as two unrelated categories.
The four values are not named consistently: tcp_ip uses an underscore while http-failure and http-diff use hyphens. That is how upstream defines them, so copy them verbatim rather than tidying them up.
All four *_match fields are null in the anomalous measurement, because the DNS stage already failed, no connection was made, and there was nothing left to compare. When you see a row of null, go back and find the first failure field with a value in the measurement flow. That is where the problem happened.
Anomalous does not mean blocked
A value in blocking only means this measurement disagreed with the control. Establishing that network interference actually occurred takes more evidence. In the anomalous measurement above, the Probe's A and AAAA queries for ntc.party both returned dns_nxdomain_error (domain does not exist), yet at the time of writing the domain resolved to an IPv6 address through several public resolvers. A single measurement cannot separate network interference from a resolver's momentary state or a change in the domain's own configuration.
Concluding that something is blocked needs measurements cross-referenced over time, across ASNs and across resolvers. For the full breakdown of the verdict mechanism and where misreadings come from, see How OONI decides a site is blocked. For quality control at the dataset level, see OONI is guarding its data against bad measurements.
The evidence: the raw record behind the verdict¶
Several more fields under test_keys record every network operation the measurement performed. Each has its own specification:
| Field | Contents | Specification |
|---|---|---|
queries |
Question, answer and failure for every DNS query | df-002-dnst |
tcp_connect |
Target and outcome of every TCP connection attempt | df-005-tcpconnect |
tls_handshakes |
Parameters, certificates and outcome of every TLS handshake | df-006-tlshandshake |
requests |
Full content of every HTTP request and response | df-001-httpt |
network_events |
Timed events across the connection, for analysing latency and where things broke | df-008-netevents |
control |
The control's equivalent records, structured to mirror the Probe side. Every verdict field is derived from the difference between the two | ts-017 |
failure strings throughout |
Unified naming for every failure reason, such as dns_nxdomain_error, generic_timeout_error and ssl_unknown_authority |
df-007-errors |
Laying the two measurements' queries side by side shows exactly what the verdict rests on:
{
"hostname": "presidentlee.tw",
"query_type": "A",
"failure": null,
"answers": [{"answer_type": "A", "ipv4": "43.254.17.201"}]
}
{
"hostname": "ntc.party",
"query_type": "A",
"failure": "dns_nxdomain_error",
"answers": []
}
dns_consistency in the verdict layer is the conclusion. queries and control are what it is based on. To check whether a verdict is reasonable, go down to the evidence layer.
Versions and compatibility¶
Checking versions before reading the spec saves a good deal of confusion:
data_format_versionis currently0.2.0. Envelope field definitions are stable, so df-000-base can be read directly against real data.- The
test_versionactually in circulation forweb_connectivityis0.4.3. ts-017 has been updated to describe the v0.5 algorithm, while production still runs mostly on v0.4. The spec requires a new algorithm to use different test_keys fields, so the v0.4 definitions stay compatible and the field readings above apply to both versions. - Fields starting with
x_are not in the spec. Real data containsx_dns_runtime,x_status,x_th_runtimeand others. They are experimental implementation extensions that come and go between versions, so analysis code should not depend on them.
The spec's master branch has had no new merges since 2025-06, though issues and pull requests are still under discussion, including proposals for an ICMP data format and a DPI fragmentation nettest. The spec works well as a stable reference for now. Link back to the upstream text when citing it, rather than copying specification content that will drift once upstream moves.
Where to go from here¶
With the fields understood, the next question is how the verdict fields are computed, and why a value does not mean a block.
The full upstream index is at ooni/spec, where data-formats holds the data formats and nettests holds each nettest's algorithm definition.