Measuring Performance
The SDK provides methods to perform a TCP Ping and extended Speedtest to measure the performance of locations.
Migrating: configuration is now a parameter
Breaking change. speedtest().getConfiguration() and instanceDiscovery().getConfiguration()
are removed, and MeasurementConfig is no longer part of the public API. Every measurement method
now takes its configuration as a parameter.
Why
There used to be two ways to configure a measurement, and they overlapped. A long-lived
MeasurementConfig object per facade held 29 mutable settings and applied to every run; a
SpeedTestConfig record passed per call overrode 10 of them. Which one won depended on the field.
The object was also the only way to reach some settings — including the probe transport — and the C
SDK never exposed it for the speed test at all, so a C host could not run a speed test over UDP.
There is now exactly one mechanism: you pass the configuration to the call.
What to call instead
| Removed | Use instead |
|---|---|
speedtest().getConfiguration() + createSpeedtestMeasurement(subscriptionId:config:callback:) | createSpeedtestMeasurement with the settings in its SpeedTestConfig parameter |
instanceDiscovery().getConfiguration() + createSmartLocationMeasurements(subscriptionId:refresh:) | createSmartLocationMeasurements(subscriptionId:refresh:config:fastMode:) — refresh and fastMode stay parameters in their own right: refresh chooses between measuring and reading the cache, and fastMode says what you want the answer for. Neither is a question about how to measure |
instanceDiscovery().getConfiguration() + pingLocations(subscriptionId:callback:) | pingLocations with a PingLocationsConfig parameter |
speedtest().getConfiguration().setUdpProbePort(...) + probeUdp(subscriptionId:) | probeUdp with a UdpProbeConfig parameter |
MeasurementConfig as a type | The per-method config record for the method you are calling |
Each record carries only the settings its method actually uses, rather than the union of all 28. Where you relied on a setting persisting across runs, keep the record and pass it again — holding a config record is the direct replacement for holding the configuration object.
The Smart Locations record is called SmartLocationSearchConfig, not SmartLocationConfig. The
shorter name belonged to the type that became MeasurementConfig and is now retired, so reusing it
would have meant one identifier with two meanings across two releases. The longer name is also the
accurate one: most of what it configures is the batch search.
The fields on each record
Every field below is optional except endpointSelectionMode, and unset always means "use the
value we recommend".
Shared probe settings
Four settings describe how you probe this network rather than what you are measuring, so they recur on more than one record. They mean the same thing everywhere and are described once here.
| Field | On | Meaning |
|---|---|---|
probeMode | all three measurement records | How the run probes — Auto, Udp or Tcp. One transport for the whole run: the latency phase and the packet-loss phase both use it. See Choosing the probe transport. |
udpProbePort | all three, and it is the whole of UdpProbeConfig | Where the UDP echo service listens. Port detection may override it for the run — except on pingLocations, which runs none. |
pingSimultaneousConnectionLimit | all three measurement records | TCP probes in flight at once. Ignored on UDP, which uses a single socket. |
latencyProbeTimeoutMs | speed test, ping | How long one latency probe waits before it counts as no sample. Latency only — the packet-loss phase derives its own deadline from the measured RTT. |
SpeedTestConfig — 19 fields
| Field | Meaning |
|---|---|
endpointSelectionMode | Required. Automatic, or Preferred { locationId }. A preference, not a pin: a location the run cannot measure — geolocated, without test IPs, or with every test IP belonging to the connection in ignoreEndpointIps — is substituted by the nearest one it can, and the result's clusterInstance.location.id says which. |
ignoreEndpointIps | The IPs of the VPN connection the device is on — the server IP, and for WireGuard the auth IP too. Set it on every run made while connected; see Measuring while connected. Unset means disconnected. |
| (the four shared probe settings above) | |
candidateProbeSamples | Probes per candidate endpoint — what ends the candidate phase. Recommended 100. The duration follows: samples × interval, and the interval is per transport (25 ms TCP, 10 ms UDP), so ~1.0 s over UDP and ~2.5 s over TCP. |
candidatePhaseMaxMs | Ceiling on the candidate search, for a path too slow to reach that target — a TCP connect is awaited inline, so a 60 ms endpoint is probed every 60 ms whatever the interval says. It used to be the stop condition, which is why the phase cost three seconds on a fast link. |
idlePingDurationMs | The unloaded-latency window. Skipped entirely when the transfer is too short for a loaded counterpart to exist. |
minNumberCandidatesMeasurement | How many candidates must have answered before the search stops reaching further. Recommended 5. |
minNumberGoodCandidates | How many of those must be good. Recommended 1 for a speed test, which is choosing one endpoint. |
candidateSearchBudgetMs | The search's only ceiling. Recommended 10000. |
bandwidthDurationModel | How each transfer decides it is done: Auto (recommended) ends it once the reported figure has settled, Fixed always runs the full bandwidthDurationMaxMs. |
bandwidthDurationMaxMs | Ceiling on each transfer, and its exact duration under Fixed. Every 100 ms window counts towards the reported figure, which is the 90th percentile of the windows — see the note below. |
performHttpDownloadTest | Run the download transfer. |
performHttpUploadTest | Run the upload transfer. Turning a direction off removes its phase and leaves that figure unset. |
speedtestDownloadSize | Which test file the transfers use. |
speedtestSimultaneousConnections | Parallel connections each transfer opens. |
packetLoss | The packet-loss phase, as one nested SpeedTestPacketLossConfig (below). Unset is the recommended phase; a record with every field unset means the same. |
SpeedTestPacketLossConfig — 3 fields, nested under SpeedTestConfig.packetLoss
The phase has no transport of its own: it probes over the run's probeMode, and the two pacing
blocks below are how you shape it for whichever of the two that turns out to be.
| Field | Meaning |
|---|---|
tcpPacing | How the train is paced when it runs over TCP (below). Unset is the recommended pacing for TCP. |
udpPacing | The same, for UDP. Unset is the recommended pacing for UDP. |
maxDurationMs | Ceiling on the whole loss phase, drain included. Hitting it costs resolution, not correctness. |
SpeedTestLossPacingConfig — 4 fields, nested under packetLoss.tcpPacing / packetLoss.udpPacing
| Field | TCP recommends | UDP recommends | Meaning |
|---|---|---|---|
probeCount | 600 | 1000 | Probes this transport sends, and therefore the resolution: loss lands on multiples of 1/count. It lives here because it and the rate are the halves of one duration — at UDP's 1000/s a thousand probes take a second, at TCP's 50/s they would take twenty and be cut off by maxDurationMs. 0 switches the phase off for this transport; set it in both blocks to switch it off whatever probeMode resolves to. |
probesPerSecond | 50 | 1000 | Send rate. What the target tolerates, not a speed dial: every TCP connect costs the server a SYN-backlog entry, and a real edge answered only 2.2 % of a 1000/s train — its flood protection, not the link. A datagram costs it nothing per packet. |
batchSize | 1 | 10 | Probes released together before the pacing pause. A TCP probe holds a socket while in flight, so a large batch is a large number of sockets; a datagram holds nothing. |
drainMs | derived | 3000 | Quiet time after the last probe. TCP needs none — unset derives it from the probe deadline — while a UDP burst's tail is still arriving when the sending stops. |
A smaller batch is a better measurement, up to a point. At the same rate, a batch of 1 measures the
path's baseline drop rate while a batch of 100 measures whether it can absorb a hundred-packet burst —
and the larger the batch, the more of the reported loss the measurement caused itself. The floor on
going smaller is the timer: the pacing interval is batchSize / probesPerSecond and cannot go below
1 ms, so a batch of 1 cannot exceed 1000 probes per second whatever you set the rate to. Below
about 200/s use a batch of 1; above it, use the smallest batch that keeps the interval clear of a
millisecond. The SDK log prints the effective rate alongside the requested one when the two differ.
Two blocks, because there is no one right pacing. The pacing used to be three flat fields shared
by both transports, and setting any one of them pinned all three for whichever transport the run
ended up on — so a UDP phase given the TCP-shaped 200/s took five seconds over a measurement that
fits in one, with nothing to warn you. Fields are now taken individually: naming only
udpPacing.batchSize still gets the recommended rate and drain for UDP, and leaves TCP untouched.
Both blocks are read at the moment the phase runs, not when the config is converted. That matters because the transport can still change: a UDP phase that finds no reachable echo port is demoted to TCP before it measures, and is then paced like TCP.
How the two numbers combine. The pause between batches is batchSize / probesPerSecond, so the
average rate is probesPerSecond whatever the batch. 600 probes at 100/s in batches of 100 is six
batches one second apart — five seconds of sending, because the first batch goes out immediately —
plus the drain. The same 600 at the UDP recommendation is 60 batches 10 ms apart: 0.6 s. Note that
batchSize also raises the in-flight ceiling to 2 × batchSize, so a large batch over TCP can push a
process past its file-descriptor limit; probes that fail locally are excluded from both the numerator
and the denominator, so the rate stays honest but rests on fewer probes than you asked for.
How the bandwidth figure is computed. Each transfer samples throughput in 100 ms windows, summed across its
parallel connections, and reports the 90th percentile of those windows in avgMbitPerSec — the same statistic
Cloudflare's speed test shows. There is no ramp-up phase to configure: the climb at the start of a transfer stays in
the samples, and the percentile is what makes it harmless. Expect the value to sit at the upper edge of what the line
sustains rather than in the middle of its wobble; dips are reported through packet loss and loaded latency instead.
The field is still called avgMbitPerSec for compatibility.
SmartLocationSearchConfig — 16 fields
| Field | Meaning |
|---|---|
(probeMode, udpProbePort, pingSimultaneousConnectionLimit from above) | |
maxLocationsToReturn | How many answered locations reach the returned list. Not a search bound — the stop condition below is. |
minNumberCandidatesMeasurement | How many locations must have answered before the search stops reaching further. Recommended 5. |
minNumberGoodCandidates | How many of those must be good. Recommended 5 here, against the speed test's 1: this product is a list you pick from, and a list with one usable entry is not a list. |
searchBudgetMs | Wall-clock budget for the whole search. This, and nothing geographic, is what ends an unsuccessful one — so a user whose nearest server is thousands of kilometres away still gets an answer. |
probeSamples | Probes per candidate per round — what ends each round. Recommended 20: enough to rank a location, which is what this search produces, and ~200 ms per round at the UDP probe interval. The speed test recommends five times as many because its selection also demotes lossy endpoints and needs the denominator. |
probeWindowMs | Ceiling on each round, for a path too slow to reach that target. Recommended 500. It used to be the round's length, spent in full. |
probeTimeoutMs | A probe with no answer by this point is a lost probe, not a slow one. Recommended 500. |
performHttpDownloadTest | Run the throughput phase at all. |
performHttpUploadTest | Recommended false: the ranking reads no upload figure. |
bandwidthDurationModel | fixed or auto. Recommended fixed — a ranking probe with a known cost per candidate. |
bandwidthDurationMaxMs | Exact transfer length under fixed, ceiling under auto. Recommended 300. |
downloadSize | Recommended large, so a fast location is not capped by the file running out. |
simultaneousConnections | Recommended 2 — fewer than the speed test's, because many locations are measured in one window. |
The last eight of those are new. They were previously fixed for you, reachable only from an in-process Rust entry point, which meant the two settings that make this search's known 500 ms / 500 ms probe defect visible could be described but not investigated. Leaving them unset is still the right default — they are what makes the search quick enough to run before a connection, and the recommended values here are deliberately not the speed test's.
fastMode and performHttpDownloadTest: false are not two spellings of the same thing.
fastModeis a parameter on the call, and it isminNumberGoodCandidates: 1plus no throughput phase. It answers with an endpoint as soon as one good one is found — in practice after the user's own country.performHttpDownloadTest: falsereturns the full ranked set, measured across the whole search, just without a transfer on each one.
Both skip the throughput phase, which is where a Smart Locations run spends most of its time, so both are much faster. But one answers "give me an endpoint" and the other answers "rank everything, quickly". Pick by which question you are asking.
fastMode fills only what you left unset: ask for it and also name minNumberGoodCandidates and
you get the number you named. It is a parameter rather than a field because it is not a setting — it
says what you want the answer for, and moves two settings to suit.
Either way, every location comes back with download and upload unset and the downloads use case
unscored; the other five still score, because a use case with no data is excluded from the overall
mean rather than averaged in as a zero.
What a run decided: ClusterMetrics.debug
Every measurement comes back with an optional MeasurementDebugInfo, and it answers questions the
rest of the result cannot:
| Field | What it tells you |
|---|---|
probeModeRequested / probeModeResolved | Which transport you asked for, and which one ran. auto → tcp means this network is on record as not answering UDP. |
probeFallbackArmed / probeFallbackFired | fired means UDP reached nothing and the whole measurement was repeated over TCP — the run took roughly twice as long as its numbers suggest. armed: false on a udp result means you commanded UDP, so a failure fails the run. |
tcpPingPort / tcpDownloadPort | The ports detection found on this network. |
candidateBatches / candidatesMeasured | One batch is normal; more means the near endpoints were not good enough and the search had to reach further. |
selectionMs, unloadedLatencyMs, downloadMs, uploadMs, packetLossMs, totalMs | Where the time went. |
Diagnosis only. Do not branch product behaviour on any of it, and do not cache or compare on it. It says what happened during one run, not how good an endpoint is — that is the ranking's job.
The phase durations are measured where the phases are called, so they include the setup each does
before its first sample. They will not add up to totalMs, and the difference is not an error:
it is the run's own overhead — the instance catalog, the geolocation lookup, the work between phases.
A large remainder is worth reporting.
debug is null when the run did not collect it, which is the case for a ClusterMetrics decoded
from a cache or a recording written before this release.
PingLocationsConfig — 6 fields
| Field | Meaning |
|---|---|
| (the four shared probe settings above) | |
tcpPingPort | The port TCP probes connect to. Meaningful here and nowhere else: this is the only measurement that runs no port detection, so on every other path the detected port wins. |
probeWindowMs | How long the sweep measures for. Named for what it is here — a ping sweep has no candidate phase to bound. |
UdpProbeConfig — 1 field
| Field | Meaning |
|---|---|
udpProbePort | Where the UDP echo service listens. |
Nothing bounds the candidate set geographically. The search takes batches of the nearest untried
locations, the user's own country first, and stops once minNumberCandidatesMeasurement have answered
with at least minNumberGoodCandidates good among them. candidateSearchBudgetMs is the only ceiling.
There used to be a maxDistanceKm radius with a numberOfEndpointCandidates cap on top. Both are gone:
a radius bound does not only limit cost — for a user whose nearest usable server is 1200 km away it
turned "a slower answer" into "no answer", and the cap it came with once excluded a border user's entire
own country. The budget covers the case they were there for, and cannot fail that way.
Two things a run still decides for itself
Neither is a preference, so neither is a field on any record:
- A speed test returns a single measurement. That is the method's signature, not a setting.
- The TCP ports are detected per run. A port the network blocks is not something a caller can choose. The same is true of the fixed timings of a Smart Locations run, which are constants of that measurement rather than settings — see the note in the diagnostic CLI's README.
Do not confuse this with sdkManager.getConfiguration(). That one is the SDK-wide configuration
object — domains, network name, state callback — and it is unchanged. Only the two
measurement configuration accessors are removed. After this release there is exactly one
getConfiguration in the API instead of three, so check the receiver before deleting a call.
Getting the recommended values
Every field on every config record is optional, and leaving a field unset means "use the value we recommend". So the zero value of each record is the recommended configuration:
| Language | Recommended configuration |
|---|---|
| Rust | SpeedTestConfig::default() |
| Swift / Kotlin | SpeedTestConfig() |
| Go | SpeedTestConfig{} |
| C# / Dart | new SpeedTestConfig() / SpeedTestConfig() |
| C | {0}, or csdk_speedtest_recommended_config(api, &config) (out-parameter, returns CSDKResultCode) |
Optional-everywhere is deliberate rather than incidental. Literal per-field defaults would not survive the trip into every binding — Go has no default field values, and the C# and Go generators run two metadata versions behind the Rust side — so a Go or C caller writing the zero value would have got a 0 ms window and 0 candidates: a silently broken run, in exactly the bindings this change set out to fix. With every field optional, the zero value means the same thing in all six languages.
To read the recommended numbers rather than just inherit them, call
speedtest().recommendedConfig() or instanceDiscovery().recommendedConfig(). These return the same
record with the fields populated, so you can show a user what a run will do, or take the
recommendation and change one value. This is also how C reaches the defaults.
Three fields come back still unset, on purpose: the packet-loss pacing blocks —
packetLoss.tcpPacing and packetLoss.udpPacing — and packetLoss.probeCount. Their recommendation
genuinely is "whatever suits that transport", and the two transports differ by an order of
magnitude, so filling any of them in would state one transport's shape as though it were the answer
for both.
This matters if you take a recommended config, change one unrelated field and pass it back: had these
carried numbers, you would have pinned a pacing without meaning to. Leave them unset unless you are
deliberately shaping the probe train — and if you want to show a user what a transport resolves to,
the values are in the SpeedTestLossPacingConfig table above.
C SDK
The C surface changes shape rather than shrinking in capability. The ~58
csdk_instance_discovery_get_* / csdk_instance_discovery_set_* configuration accessors are
removed and replaced by one config struct per method — CSDKSpeedTestConfig,
CSDKPingLocationsConfig, CSDKUdpProbeConfig and CSDKSmartLocationSearchConfig — each passed as a
parameter, plus a csdk_*_recommended_* constructor for each of the four — two per facade, named
in the table under Who frees the config.
Two things C gains that it never had: a speedtest configuration surface (previously the accessors
existed only on the InstanceDiscovery facade, so a C host could not set the probe transport for a
speed test), and a reachable UDP probe port for csdk_speedtest_probe_udp, which until now read a
port only the unreachable Speedtest-side configuration held.
CSDKSpeedTestConfig cfg = {0}; is a valid, correctly configured request. It works because every
field is optional and all-zero is all-null, which means "use the recommended value" — that is exactly
why the underlying records are optional-everywhere rather than carrying literal defaults.
Who frees the config
A measurement takes its config as a pointer — CSDKSpeedTestConfig* and so on — and only reads
through it. The call never takes ownership, so freeing the record stays your job, and whether there
is anything to free depends on where the record came from. The two cases are indistinguishable at
the call site, and only one of them leaks, so decide it where you create the struct:
- A struct you declare yourself is stack memory.
CSDKSpeedTestConfig cfg = {0};goes away with its scope, like any local. Never pass it to acsdk_free_*function. - A struct from a
csdk_*_recommended_*constructor is heap memory. It is fully populated, and every field it fills is its own allocation, so release it with its matchingcsdk_free_*— that single call takes the fields with it.
The four constructors follow the same shape as csdk_configuration_domains_for_environment: an
out-parameter, plus a CSDKResultCode return.
| Constructor | Struct | Release with |
|---|---|---|
csdk_speedtest_recommended_config | CSDKSpeedTestConfig | csdk_free_speedtest_config |
csdk_speedtest_recommended_udp_probe_config | CSDKUdpProbeConfig | csdk_free_udp_probe_config |
csdk_instance_discovery_recommended_smart_location_search_config | CSDKSmartLocationSearchConfig | csdk_free_smart_location_search_config |
csdk_instance_discovery_recommended_ping_locations_config | CSDKPingLocationsConfig | csdk_free_ping_locations_config |
CSDKUdpProbeReport* report = NULL;
// Stack: the recommended run, and nothing to free.
CSDKUdpProbeConfig cfg = {0};
csdk_speedtest_probe_udp(api, subscription_id, &cfg, &report);
// Heap: read or adjust the recommended values, then release the record.
CSDKUdpProbeConfig* recommended = NULL;
if (csdk_speedtest_recommended_udp_probe_config(api, &recommended) == CSDKResultCodeOK) {
csdk_speedtest_probe_udp(api, subscription_id, recommended, &report);
csdk_free_udp_probe_config(recommended);
}
Getting it the wrong way round fails in both directions: passing a stack struct to a csdk_free_*
function frees memory the allocator never handed out, and skipping the call on a constructor's
struct leaks the record together with every field in it.
Choosing the probe transport
The transport travels with the call, and PingMode now has three values rather than two.
probeMode | What happens |
|---|---|
Auto | Probes over UDP where the network allows it and falls back to TCP where it does not. The verdict is remembered per network, so a UDP-blocking network costs the discovery once rather than on every run. |
Udp | UDP, and only UDP. No detection, no fallback, nothing remembered. A network that blocks UDP yields no samples at all. |
Tcp | TCP. Works everywhere; measures a fleet in waves rather than all at once. |
| unset | The SDK decides — the recommendation, and the same behaviour as Auto. |
Both facades behave identically. The speed test gains the same per-network memory that Smart Locations already had.
This changes behaviour on upgrade, with no code change on your side. Every host that sets nothing
gets TCP today and will get UDP where it works after upgrading. It applies to the latency
figures and the packet-loss figures alike — one probeMode governs both phases, so they move
together by construction — so if you track either across the upgrade, expect a step change in
both.
The change is deliberate. The per-network fallback it relies on is not new — it is the mechanism Smart Locations has already been running in production — and the alternative, keeping TCP as the recommendation, would have meant nobody ever received the improvement without knowing to ask for it. That is precisely the situation this release exists to end.
If you want TCP, write Tcp. Relying on the default to mean it no longer works.
The two transports report two different things, and neither is wrong.
- UDP reports the raw per-packet loss. A datagram is never retransmitted: what the echo does not
return was dropped. There is no per-probe deadline: the whole train is fired, the drain
(
drainMs, 3 s by default) is waited out, and a probe is lost exactly when the drain closes without its echo. Against a link conditioner set to 5 % downlink loss, expect roughly 5 % here (about twice that if the conditioner drops in both directions — see the note on round trips below). Theprobe_deadline_msa UDP run reports is the phase ceiling that bounds the wait, not a per-probe cut-off. - TCP reports the effective loss a TCP application sees. The kernel retransmits a lost SYN, and one the peer then answers is a packet TCP recovered — counted as delivered, because that is what TCP does for every real connection on that link. The per-probe deadline is a fixed, ping-like 1000 ms; a connect that has not completed by then is lost. So the same 5 % conditioner reads about 2 % over TCP. That gap is TCP compensating, not the measurement failing.
Two things the TCP figure should not be read as:
- A slow path is not loss. The TCP deadline used to be capped at 200 ms so that no retransmit could count; against a server 186 ms away it cut off every reply and reported 100 % loss on a link that answered every single probe. A distant server or a VPN tunnel is now measured, not declared dead.
- An endpoint's flood protection is not loss either, although it is counted as such: each connect costs the server a SYN-backlog entry, and a 1000/s train had 2.2 % of its probes answered. The TCP pacing is conservative for that reason.
If you alert on loss rates, know which transport produced the number before comparing runs.
So the UDP figure is the truthful one and the old TCP figure was inflated. If you alert on loss rates, rebaseline after upgrading rather than reading the drop as an improvement — the change is in what the number means, not only in its value.
Auto is not the same as leaving the field unset, even though the two resolve to the same
behaviour today. Unset means "whatever we recommend", and what we recommend can change between
releases — as it just did. Auto pins the behaviour. Write it explicitly if you want the fallback
behaviour to stay put across future upgrades.
Udp is a command, not a preference — it disables the fallback. That is deliberate: it is what
lets a diagnostic compare the two transports honestly, since a "UDP" run that silently became a TCP
run would measure the wrong thing. But it means a forced Udp on a UDP-blocking network returns
nothing rather than quietly working. If you want a measurement that simply succeeds, use Auto.
The same three values apply to the packet-loss transport, which is chosen independently of the latency one: the two measurements want different things from a transport, so running TCP for latency and UDP for loss is a reasonable combination, as is the reverse.
To find out whether UDP leaves the current network before committing to it, call
probeUdp.
If you set probeMode = Udp today, you want Auto
This is the one change most likely to alter behaviour in an existing integration, and it does not look like a breaking change at the call site.
Until now, speedtest().getConfiguration().setProbeMode(Udp) was the only way to reach UDP on
the speed-test path, so every caller who wanted UDP had to write it. In the new vocabulary that same
value is a command: it now means "UDP or nothing", and on a network that blocks UDP the run fails
instead of falling back. Almost everyone who wrote Udp meant "prefer UDP", which is what Auto says.
So: carry Udp across only if you genuinely want a run that fails rather than falls back — a
transport comparison, or a diagnostic. Otherwise use Auto.
On the Smart Locations path Udp becomes genuinely selectable for the first time; today it silently
behaves the way Auto will.
Pinging Locations
A TCP Ping will measure the time of establishing a TCP connection to an endpoint. We are not using regular ICMP pings as these type of pings are many times blocked in several networks.
A TCP Ping only makes sense, if the user is currently not connected to the VPN, so that the ping result reflects the time it takes to send a TCP Ping packet from the user's location to the VPN server location. The SDK will prevent, that the ping method will be executed, if the user is currently connected to the VPN.
.png)
ping_locations
This method will perform a single ping per instance per location and will then calculate the average ping for that location,
Parameters
| Name | Type | Description |
|---|---|---|
| subscription_id | String | ID of the subscription which has a VPN entitlement. You can obtain that subscription id as described in Obtaining a VPN Subscription ID |
| callback | InstanceDiscoveryCallback | This callback interface needs to be implemented by the app, to receive updates while the test is being conducted |
Returns
Returns an array of ClusterMetrics where only the ping field is populated (download/upload/distance are empty).
Speed Test
The speed test is an extended form of what the Smart Location algorithm is doing. It first pings all locations to determine the best endpoint which in the next step should be used to perform an HTTP up- and download test. Compared to the Smart Location algorithmn, it takes much more time, as the number of pings and the time to perform the up- and download is 10 seconds each. The reason for using a fixed time is, to have stable results independent of the capacity of the connection.
The speed test lives on its own Speedtest facade, reached via SdkManager.speedtest(). Like Smart
Locations, it is split into a measure step (create_speedtest_measurement) and an evaluate
step (rank_speedtest_measurements). Cancel a running speed test with
SdkManager.speedtest().cancel_ongoing_operation().
We suggest, to warn the user when being in a cellular network, that an extended amount of data will be up- and downloaded!
create_speedtest_measurement
This will start a speedtest and will return a single ClusterMetrics measurement, once the results have been fetched. To compare protocols, call it once per protocol you want to measure, then rank the results with rank_speedtest_measurements.
Parameters
| Name | Type | Description |
|---|---|---|
| subscription_id | String | ID of the subscription which has a VPN entitlement. You can obtain that subscription id as described in Obtaining a VPN Subscription ID |
| config | SpeedTestConfig | Configuration for the run: how the endpoint is selected, plus optional overrides for this run only. Every override left unset takes whatever speedtest().get_configuration() says — which is where the same settings live for every run. |
| callback | InstanceDiscoveryCallback | This callback interface needs to be implemented by the app, to receive updates while the test is being conducted. |
Returns
Returns a single ClusterMetrics for the measured endpoint, with the ping, download and upload fields populated.
rank_speedtest_measurements
This evaluates one or more speed-test measurements and returns a best-first Ranking per use case. It is a pure, offline operation. When comparing protocols, tag each measurement with the protocol it was produced with so the ranking can identify the winning protocol.
The winning protocol is ranking.<use_case>[0] — do not determine it yourself by comparing score.
Candidates with near-equal scores are deliberately treated as tied so that download and upload speed
decide the order, so the winner may carry a slightly lower score than the runner-up. Example: two
protocols measuring 272.7 / 150 Mbit/s and 272.1 / 270 Mbit/s score within 0.0004 of each other, and
the SDK ranks the one with 270 Mbit/s upload first — picking the higher score yourself would select
the protocol with 44 % less upload on a 0.22 % download difference. See
Scoring & ranking for the full sort order.
Parameters
| Name | Type | Description |
|---|---|---|
| measurements | Array<MeasuredCandidate> | The measurements to rank. For a protocol comparison, pass one MeasuredCandidate per protocol (same location), each tagged with its real PreferredProtocol. |
| config | RankingConfig? | Optional; null is the default ranking. Carries the optional baseline — the same endpoint measured with the VPN off, which changes how throughput is judged (see below) — and the threshold of the Lightway rule (see The recommended protocol). |
RankingConfig
| Name | Type | Description |
|---|---|---|
| baseline | SpeedtestBaseline? | The unprotected line, for relative throughput scoring. null keeps the absolute curves. |
| lightway_recommendation_min_score | Float? | Score (0–5, on the recommended list) from which Lightway is recommended ahead of a higher-scoring protocol (it must also be suitable in every use case). null uses 4.5. A value above 5 disables the rule. |
Comparing protocols against the unprotected line
Without a baseline, throughput is judged absolutely: "is this fast?". That is the right question for Smart Locations and for a single measurement, but not for a protocol comparison, where a tunnel keeping a quarter of a 958 Mbit/s line would still score well simply because 250 Mbit/s is fast.
Pass a baseline and throughput is judged as the share of the line's own capacity a protocol
preserves:
let isp = try await speedtest.createSpeedtestMeasurement(...) // VPN off
let wg = try await speedtest.createSpeedtestMeasurement(...) // WireGuard
let lw = try await speedtest.createSpeedtestMeasurement(...) // Lightway
let ranking = try speedtest.rankSpeedtestMeasurements(
measurements: [
MeasuredCandidate(protocol: wireguard, metrics: wg, label: "WireGuard"),
MeasuredCandidate(protocol: lightway, metrics: lw, label: "Lightway"),
],
config: RankingConfig(
baseline: SpeedtestBaseline(metrics: isp, downloadReferenceCapMbit: nil),
lightwayRecommendationMinScore: nil
)
)
SpeedtestBaseline
| Name | Type | Description |
|---|---|---|
| metrics | ClusterMetrics | The measurement taken with the VPN off — pass what create_speedtest_measurement returned, unmodified. |
| download_reference_cap_mbit | Float? | The throughput at which more bandwidth stops being a quality difference, in Mbit/s. null uses 1000. |
The cap is not a maximum speed — it is where "fast enough for anything" begins, and it keeps the comparison fair at the top of the market. On a 10 Gbit/s line a 1 Gbit/s tunnel would otherwise be scored as a 90 % loss, when in fact it is excellent and a provider capping capacity there is doing nothing wrong. Anything reaching the cap scores full marks, however fast the line beneath it is.
Raise it for stricter scoring, lower it for more forgiving. It is exposed so you can tune it without waiting for an SDK release.
Two things the baseline is not: it is not a candidate — it never appears in a ranked list and can
never be returned as a use case's winner ("best for gaming: no VPN") — and it does not affect
streaming, browsing or video-conf, whose thresholds stay absolute because 250 Mbit/s really is as
good as 958 for a 4K stream. The difference shows up where it is real: in downloads, and in the
all-rounder average behind recommended.
Measuring while connected
A speed test run while the VPN is up has to send its traffic through the tunnel, and there is one address for which that can never happen: the server the tunnel terminates on. The VPN client installs a bypass route for it — it has to, or the tunnel would encapsulate itself — so anything sent to that IP takes the physical interface. A speed test that happens to pick that server's test IP therefore measures the unprotected line and reports it as the tunnel, and the preferred cluster is exactly where that happens, because it is the one you are connected to.
The SDK cannot see the connection — it hands out endpoint lists, the app picks one and connects —
so the app says: put every IP the connection uses into ignoreEndpointIps on every run made
while connected. The run removes them (and the public IP the geolocation service reports) from
the cluster catalog before choosing an endpoint. The preferred cluster is still measured, on one of
its other test IPs; a location left without one falls back to the nearest that kept one. What was
removed is listed on ClusterMetrics.debug.excludedTestIps.
Two flows, depending on what the user picked in the location picker. In both, the baseline and the connected runs end up on the same cluster, and the connected runs never measure the tunnel's own server.
Scenario 1: the user selected "Best Location" in the location picker
- Run the baseline measurement with
Automatic(VPN off). - Read the location the baseline was measured against from the result
(
clusterInstance.location.id). - Connect the VPN to that location.
- Run the speed test with
Preferred { locationId }from step 2 andignoreEndpointIpsset to the IPs of the connection from step 3.
Connect to the location from step 2, not to whatever the app's own "Best Location" would pick:
Automatic measured the nearest well-reachable cluster, which is usually but not necessarily the
same one, and the comparison only holds if both runs hit the same cluster.
Scenario 2: the user selected a specific location in the location picker
- Run the baseline measurement with
Preferred { locationId }from the location picker (VPN off). - Connect the VPN to that location.
- Run the speed test with the same
Preferred { locationId }andignoreEndpointIpsset to the IPs of the connection from step 2.
Scenario 2 in code:
let sf = SpeedTestEndpointSelectionMode.preferred(locationId: "san-francisco")
// 1. Baseline — VPN off, nothing to ignore.
let isp = try await speedtest.createSpeedtestMeasurement(
subscriptionId: id,
config: SpeedTestConfig(endpointSelectionMode: sf),
callback: cb
)
// 2. Connect to the same location; the app knows which endpoint it picked.
let endpoint = try await connectVpn(locationId: "san-francisco") // your VPN client
// 3. Each protocol — same cluster, never the server the tunnel runs on.
let wg = try await speedtest.createSpeedtestMeasurement(
subscriptionId: id,
config: SpeedTestConfig(endpointSelectionMode: sf, ignoreEndpointIps: endpoint.bypassedIps),
callback: cb
)
bypassedIps is every address the client routes past the tunnel for that connection: the server IP,
plus the authIp for WireGuard. Passing more than needed costs nothing; passing less than the
bypass route covers is the bug this field exists to prevent. The list is applied only while the
SDK's geolocation lookup reports the device connected; it says which addresses to avoid, not
whether there is a tunnel, so passing it while disconnected changes nothing.
The recommended protocol
ranking.recommended is the list to recommend from, and its first entry is the recommendation. By
default that is the best all-rounder — the mean of the five use-case scores, ordered by the same
banded key as every other list. One rule sits on top of the measurement:
Lightway is recommended whenever it performs at a high level — even if another protocol measured a
little better. "High level" means two things at once: a Lightway candidate (TCP or UDP) scores at
least 4.5 on the recommended list (the Excellent boundary) and is suitable — rated Good
or better — in every use case. Then it is moved to the front; everything behind it keeps its order.
The reason is security. Lightway is our own protocol and brings security properties a speed test cannot see. Once it is in that top band, a small edge for another protocol — a few hundredths in favour of WireGuard, say — is not a difference a user would notice, so Lightway is the protocol of choice. Below the threshold the measurement alone decides, exactly as before.
Three things to know about the rule:
- Only
recommendedchanges. Every use-case list,score,categoryandsuitableflag stays exactly as measured, so the recommended protocol may not be the winner of any single use case — showing "Recommended: Lightway" next to "Best for downloads: WireGuard" is correct, not a bug. - The bar is a setting.
RankingConfig.lightway_recommendation_min_scoreoverrides 4.5; raise it to demand more of Lightway before it is preferred, lower it to prefer it sooner, set it above 5 to switch the rule off. Leave itnullto take the SDK default. The suitability condition is not configurable: the overall score is a mean, and a mean hides a single weak axis — four perfect use cases carry a Lightway whose jitter makes gaming merely Ok to an average of 4.6. Recommending it would break the promise above for exactly the users who would notice, so it is never done. - Smart Locations is unaffected. Its candidates carry no protocol (
PreferredProtocol.none), so the rule never fires there.
let wireguard = PreferredProtocol.wireGuard(obfuscation: WireguardObfuscation.none)
let lightwayUdp = PreferredProtocol.lightway(
transport: ProtocolTransport.udp,
obfuscation: LightwayObfuscation.none
)
// Same endpoint selection for every run, so the comparison isolates the protocol.
let config = SpeedTestConfig(endpointSelectionMode: mode)
let wg = try speedtest.createSpeedtestMeasurement(subscriptionId: id, config: config, callback: cb)
let lw = try speedtest.createSpeedtestMeasurement(subscriptionId: id, config: config, callback: cb)
let ranking = try speedtest.rankSpeedtestMeasurements(measurements: [
MeasuredCandidate(protocol: wireguard, metrics: wg, label: "WireGuard"),
MeasuredCandidate(protocol: lightwayUdp, metrics: lw, label: "Lightway UDP"),
])
// The recommendation: the best all-rounder, or Lightway once it is Excellent overall and
// suitable for every use case — see
// "The recommended protocol" above.
let recommended = ranking.recommended.first?.protocol
let bestForGaming = ranking.gaming.first?.protocol
// Every protocol that is fit for gaming at all (rated Good or better on it) — usually more than
// one, and possibly none even though a winner is named.
let suitableForGaming = ranking.gaming.filter { $0.suitable }.map { $0.protocol }
PreferredProtocol is an enum with associated values — the transport and the obfuscation are part
of the protocol's identity, because they are part of what was measured. There is no bare wireguard
case. PreferredProtocol.none is the one exception, and it is what Smart Locations uses.
A single speed test is just one measurement: rank_speedtest_measurements([MeasuredCandidate(protocol: protocol, metrics: metrics, label: "My VPN")]). The QoS categories are available as ranking.<use_case>[i].category.
Returns
A Ranking.
explain_ranking_with_ai
Turns a finished Ranking into a structured, plain-language explanation
a non-technical user can read: a short glossary of what was measured, one verdict per measured
protocol, and — per use case — which protocol won and why. It does not re-rank anything; the
engine's ordering is handed to the model as fact. Pass the RankingConfig the ranking was produced
with and, if it carries a baseline, the user's own line is explained in the same request and
every protocol is read against it — see Explaining protocols against your own
line.
The full completion is streamed internally and parsed for you: the call returns the finished object, not a token stream. Expect it to take a few seconds.
Requires both the SpeedTest and CategoryExpressAi licenses, and a subscription_id holding an AI
entitlement — see Express AI.
Parameters
| Name | Type | Description |
|---|---|---|
| subscription_id | String | ID of a subscription with an AI entitlement. Must not be empty. |
| ranking | Ranking | The ranking to explain — pass the object rank_speedtest_measurements returned, unmodified. The label you gave each MeasuredCandidate is what the texts call it. |
| config | RankingConfig? | The same config the ranking was produced with — what you passed to rank_speedtest_measurements, or null if that was null. With a baseline in it, the line travels in the same request and the result's baseline is filled. No binding-level default: pass null explicitly. |
| model | String? | ExpressAI model code. null uses the default ("oai"). |
| language | ExpressAiLanguage? | Output language of every human-readable string. null means English. JSON structure, measurement_ids and the app-given labels are never translated. |
ExpressAiLanguage covers 18 languages: English, Danish, German, Spanish, Finnish, French, Italian,
Japanese, Korean, Norwegian Bokmål, Dutch, Polish, Portuguese, Portuguese (Brazil), Russian, Swedish,
Thai and Turkish.
let ranking = try speedtest.rankSpeedtestMeasurements(measurements: candidates, config: nil)
let explanation = try await speedtest.explainRankingWithAi(
subscriptionId: id,
ranking: ranking,
config: nil, // the same config the ranking got
model: nil, // ExpressAI default
language: .german
)
Returns
A SpeedtestRankingExplanation.
| Name | Type | Description |
|---|---|---|
| glossary | Array<GlossaryEntry> | The handful of measured things — responsiveness, steadiness, dropped data, speed — in plain words. Roughly one entry each; treat the count as variable. |
| baseline | BaselineVerdict? | The user's own line, present exactly when config.baseline was passed. Its verdict says what the line is like and what the protocols made of it. null otherwise. |
| protocols | Array<ProtocolVerdict> | One verdict per measured protocol. Same entries as the ranking, in the engine's overall order. |
| best_for | BestForUseCase | The winner of each use case, with the model's reason. |
| recommended | UseCaseWinner | The engine's recommendation — the first entry of Ranking.recommended (see The recommended protocol). Its why is always empty — see the note below. |
GlossaryEntry
| Name | Type | Description |
|---|---|---|
| term | String | Plain name with the technical word in brackets, e.g. "Responsiveness (latency)". |
| description | String | One plain sentence on what it is. |
| importance | String | Which activities depend on it, and why. |
BaselineVerdict — no measurement_id and no label: the baseline is a reference the protocols
were scored against, never a ranked candidate.
| Name | Type | Description |
|---|---|---|
| verdict | String | Two or three sentences: what the line is like, then what the measured protocols made of it — what they cost, what they kept, whether one improved on it. |
| strengths | String | What the line does well. |
| weaknesses | String | Where the line itself is weak — the part every tunnel inherits — or the language's equivalent of "None worth noting." |
ProtocolVerdict
| Name | Type | Description |
|---|---|---|
| measurement_id | UInt32 | Identifies the measurement this verdict is about — matches RankedCandidate.measurement_id in every list of the ranking. |
| label | String | The app-given label of that measurement, copied from your MeasuredCandidate. Empty if you passed none. |
| verdict | String | One plain line on what this protocol is like to use. |
| strengths | String | One or two sentences on what it does well. |
| weaknesses | String | One or two sentences on where it struggles, or the language's equivalent of "None worth noting." |
BestForUseCase — one UseCaseWinner per field: gaming, video_conf, streaming, browsing,
downloads.
UseCaseWinner
| Name | Type | Description |
|---|---|---|
| measurement_id | UInt32 | The winning measurement — the first entry of that use case's list in the ranking. Computed by the SDK, not by the model. |
| label | String | That measurement's app-given label. |
| why | String | A few plain words on why it suits that activity. |
Explaining protocols against your own line
If you measured the line with the VPN off and ranked the protocols against it
(RankingConfig.baseline), pass that same config here. One
request then does what used to take two: the line goes to the model next to the protocols, rated on
its own terms, and the texts read every protocol against it —
- what the tunnel costs: the share of the line's speed it keeps, the delay it adds;
- where it is at least as good — and where it is better: a protocol that reaches the server faster than the bare line found a better-routed path than the provider's, and the text says so as a measured fact ("the detour through the VPN actually cut the delay"), not as a promise;
- which weaknesses are the line's, not the protocol's: jitter, packet loss and bufferbloat already in the baseline are named once, as the line's own, instead of being pinned on every protocol.
The result gains a baseline object describing the line itself; the protocol verdicts stay in the
same shape, just written with that context. Render it as one more card, above the protocols.
// 1. VPN off — the line itself.
let line = try await speedtest.createSpeedtestMeasurement(subscriptionId: id, config: cfg, callback: cb)
// 2. VPN on, once per protocol …
let config = RankingConfig(
baseline: SpeedtestBaseline(metrics: line, downloadReferenceCapMbit: nil),
lightwayRecommendationMinScore: nil)
let ranking = try speedtest.rankSpeedtestMeasurements(measurements: candidates, config: config)
// 3. Same config → the line is explained in the same request.
let explanation = try await speedtest.explainRankingWithAi(
subscriptionId: id, ranking: ranking, config: config, model: nil, language: .german)
let line_summary = explanation.baseline?.verdict // the line, described once
Keep the baseline out of measurements — it is the reference, not a candidate. And a ranking
that holds only the line (the baseline case below) is itself the baseline: passing a baseline
config with it fails with InvalidInputParameters.
Two kinds of ranking, one method
explain_ranking_with_ai explains two different things, and picks which from the ranking itself
— there is no second method and nothing extra to pass:
| The ranking holds | What you get back |
|---|---|
| Several measurements, or one over a real protocol | A protocol comparison: one verdict per protocol, a winner named per use case. |
Exactly one measurement tagged PreferredProtocol.none | A baseline read: what the user's own line is like, and what that means for running a VPN over it. |
The shape of the result is identical either way — only the texts differ, so one rendering path serves both.
The baseline case
This is the AI companion to the baseline step of a network analysis, next to the routing, DNS and
adapter checks an app does itself. Measure with the VPN off, then rank that single measurement
tagged .none — the variant meaning "no protocol involved":
// VPN off. Same endpoint selection you would use for a protocol comparison.
let metrics = try await speedtest.createSpeedtestMeasurement(
subscriptionId: id, config: config, callback: cb)
let baseline = try speedtest.rankSpeedtestMeasurements(measurements: [
MeasuredCandidate(protocol: .none, metrics: metrics, label: "Your connection")
])
// The rating is usable here already, with no AI and no entitlement involved:
let overall = baseline.recommended.first?.category // Excellent … Poor
let gaming = baseline.gaming.first?.category
let explanation = try await speedtest.explainRankingWithAi(
subscriptionId: id, ranking: baseline, config: nil, model: nil, language: .german)
// One entry: the connection itself. Its `verdict` is the headline text.
let summary = explanation.protocols.first?.verdict
"Your connection is steady and has plenty of speed, but it is slow to react, which makes fast-paced games and video calls feel laggy. A VPN sometimes helps with exactly that: it can take a better-routed path than your provider's, so in some cases responsiveness actually improves."
Two fields read differently in this case:
protocolsholds exactly one entry — the connection. Itsverdictis the headline: two or three sentences carrying both the state of the line and what it means for a VPN.strengths/weaknessessupport it.best_foris not about winning — nothing was competed against. Eachwhysays what to expect from this line for that activity ("games will feel laggy"), calibrated to that use case's own category.
protocol never affects the score — it is only the entry's identity and the ranking's last
tie-break — so a .none measurement is rated exactly like any other.
Both halves of the baseline test matter. PreferredProtocol.none on its own does not mean "no
VPN": Smart Locations tags every candidate with it, because there the protocol is irrelevant and
locations are what differ. A ranking is treated as a baseline only when it holds exactly one
such entry, so a Smart Locations ranking stays on the comparison prompt.
The flip side: keep your baseline out of a protocol-comparison ranking. A .none entry there
competes for first place and can end up named as a use case's winner — "best for gaming: no VPN".
What the baseline text will and will not say
The prompt encodes how each measured property actually behaves under a VPN, so the text stays honest rather than promotional:
| Measured | What the text may claim |
|---|---|
| Jitter, packet loss | A VPN cannot fix these and tends to make them more noticeable. Never presented as something a VPN improves. |
Bufferbloat (loaded_latency) | Named in plain words ("the connection gets sluggish whenever something is downloading"), as a property of the line a VPN does not fix. |
| Latency | The one case where an improvement may be suggested — a VPN can take a better-routed path than the provider's. Only ever as a possibility ("in some cases"), only when latency is the weak point while steadiness is fine, and never as consolation for an unstable line. |
| Throughput | A VPN costs a little to encryption; unnoticeable on a fast line, felt on a slow one. |
It also gives no advice about changing provider or rebooting the router — the screen is about what a VPN will do on this line, not about repairing the line.
Presenting it
The intended layout is one card per protocol: its verdict, strengths and weaknesses, plus —
for every use case it won — a "best for <activity>" chip and that winner's why. Since
best_for is keyed by use case rather than by protocol, match them on measurement_id:
func wins(_ measurementId: UInt32) -> [(useCase: String, why: String)] {
[
("Gaming", explanation.bestFor.gaming),
("Video Calls", explanation.bestFor.videoConf),
("Streaming", explanation.bestFor.streaming),
("Browsing", explanation.bestFor.browsing),
("Downloads", explanation.bestFor.downloads),
]
.filter { $0.1.measurementId == measurementId }
.map { (useCase: $0.0, why: $0.1.why) }
}
A protocol can win several use cases, or none. In practice one protocol often wins all five —
the ranking's tie-break keys do not depend on the use case, so the lists only diverge when the scores
genuinely differ (see Smart Locations). Cards with no chips at all are normal,
and they are carried entirely by verdict / strengths / weaknesses.
recommended.why is always empty. The model is not asked to justify the recommendation.
measurement_id and label are still filled in, from the engine's Ranking.recommended, so the
pick stays available — render the recommendation from the ranking (see
The recommended protocol), not from a text here.
Ranking.recommended itself is unaffected and still drives Smart Location selection.
The texts never state a rank or a position ("second best", "tops the ranking"), because your app owns the ordering and renders its own badges. They describe each connection on its own terms, calibrated to its absolute quality band — a winner in a weak field is described as the least bad option, not as a good one. Render them next to your own rank badges without fear of contradiction.
Errors
| Error | When |
|---|---|
InvalidInputParameters | Empty subscription_id, or a baseline config passed with a ranking that holds only the unprotected line. |
JsonDeserializationError | The model answered with something other than the requested JSON. Carries the raw text. |
ExpressAiUnexpectedFinish | The completion stopped early (e.g. length), so the answer is incomplete. |
ExpressAiProtocol | The model's protocols[] did not mirror the ranking's measurements — an invented, missing, duplicated or reordered measurement_id — or it left out the baseline object although a baseline was passed. |
All four are transient in the sense that a retry may well succeed; none of them indicate a bad ranking. Show the ranking without the explanation rather than failing the whole screen.
C SDK
CSDKSpeedtestRankingExplanation *explanation = NULL;
CSDKError *err = csdk_speedtest_explain_ranking_with_ai(
speedtest, subscription_id, &ranking, &config /* the same CSDKRankingConfig, or NULL */,
NULL /* model */, CSDKExpressAiLanguageGerman, &explanation);
// explanation->baseline is non-NULL exactly when config carried a baseline.
// ...
csdk_free_speedtest_ranking_explanation(explanation);
csdk_free_speedtest_ranking_explanation frees the whole tree — the glossary, the optional baseline
verdict, the verdict list and every nested string. CSDKExpressAiLanguageEnglish is value 0, so a zeroed language argument means
English.
probe_udp
Answers whether UDP echo probes get out of the network this device is on. PingMode.Udp measures a
whole fleet over one shared socket with the probes in the air together; PingMode.Tcp works anywhere
but measures that fleet in waves. Guest and corporate networks that permit only ports 80 and 443 are
common enough that the difference cannot be assumed.
You do not need this to get a good measurement — PingMode.Auto discovers the same thing itself and
remembers it. Reach for probeUdp when you want the answer as an answer: to show a user why a
measurement is slow, to decide whether forcing Udp is safe before you do it, or to check a network
without running a measurement at all.
Ungated — no licence required. This is the diagnostic you run to decide what to do, so putting it behind a licence would make the decision depend on the licence it informs.
Parameters
| Name | Type | Description |
|---|---|---|
| subscription_id | String | ID of the subscription with a VPN entitlement |
| config | UdpProbeConfig | Which port to probe. Leave it at the recommended value unless your deployment moved the echo service. |
Returns
A UdpProbeReport.
| Name | Type | Description |
|---|---|---|
| available | Bool | Whether any probe came back at all. The answer, but not the whole answer — see below. |
| endpoints_probed | UInt32 | Distinct endpoints probed. |
| probes_sent | UInt32 | Datagrams sent, several per endpoint. |
| probes_answered | UInt32 | Datagrams that came back. |
| first_answer_ms | UInt32 (optional) | How long the first answer took. null when none came. Doubles as a free rough latency estimate, which a following measurement can use to size its deadlines before it has measured anything. |
Read the counts, not just available. A link that answers one probe in fifteen is available and
still a poor choice for UDP. The ratio of probes_answered to probes_sent is what separates a
network UDP sails through from one it barely survives.
It probes several endpoints on purpose — a single one being down would otherwise read as a blocked network and push every app on it onto the slower transport — and sends several datagrams per endpoint, because one datagram going missing is expected on a link that loses packets, which is exactly what this SDK sets out to measure.
What it costs
About one round trip when UDP works, and the full two-second deadline when it does not: nothing will come back, so being sure takes as long as being sure takes.
Nothing is cached. A method called probe probes. Whether the answer is worth remembering, and for how long, is yours to decide — your app knows when the device changed networks and this call does not. Note that Smart Locations keeps its own per-network memory of the transport verdict internally; that is separate from this and not shared with it.
Callback Notification Flow
While executing the speed test, you will encounter various callback types:
- Initially, you will receive several progress types labeled
pingas the speed test identifies the optimal server. This process may take a few seconds. - Once the final server instance for the speed test is selected, you will receive a progress type of
SpeedTestInstanceSelected. This notification will also provide details on Jitter, Latency, and Packet Loss as part of the results. - Subsequently, you will receive multiple progress types labeled
SpeedTestDownload/SpeedTestUpload, which will inform you of the current down- or upload speed and progress.