Skip to main content

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​

danger

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​

RemovedUse 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 typeThe 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.

info

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.

FieldOnMeaning
probeModeall three measurement recordsHow 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.
udpProbePortall three, and it is the whole of UdpProbeConfigWhere the UDP echo service listens. Port detection may override it for the run — except on pingLocations, which runs none.
pingSimultaneousConnectionLimitall three measurement recordsTCP probes in flight at once. Ignored on UDP, which uses a single socket.
latencyProbeTimeoutMsspeed test, pingHow 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​

FieldMeaning
endpointSelectionModeRequired. 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.
ignoreEndpointIpsThe 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)
candidateProbeSamplesProbes 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.
candidatePhaseMaxMsCeiling 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.
idlePingDurationMsThe unloaded-latency window. Skipped entirely when the transfer is too short for a loaded counterpart to exist.
minNumberCandidatesMeasurementHow many candidates must have answered before the search stops reaching further. Recommended 5.
minNumberGoodCandidatesHow many of those must be good. Recommended 1 for a speed test, which is choosing one endpoint.
candidateSearchBudgetMsThe search's only ceiling. Recommended 10000.
bandwidthDurationModelHow each transfer decides it is done: Auto (recommended) ends it once the reported figure has settled, Fixed always runs the full bandwidthDurationMaxMs.
bandwidthDurationMaxMsCeiling 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.
performHttpDownloadTestRun the download transfer.
performHttpUploadTestRun the upload transfer. Turning a direction off removes its phase and leaves that figure unset.
speedtestDownloadSizeWhich test file the transfers use.
speedtestSimultaneousConnectionsParallel connections each transfer opens.
packetLossThe 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.

FieldMeaning
tcpPacingHow the train is paced when it runs over TCP (below). Unset is the recommended pacing for TCP.
udpPacingThe same, for UDP. Unset is the recommended pacing for UDP.
maxDurationMsCeiling on the whole loss phase, drain included. Hitting it costs resolution, not correctness.

SpeedTestLossPacingConfig — 4 fields, nested under packetLoss.tcpPacing / packetLoss.udpPacing​

FieldTCP recommendsUDP recommendsMeaning
probeCount6001000Probes 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.
probesPerSecond501000Send 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.
batchSize110Probes 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.
drainMsderived3000Quiet 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.

info

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​

FieldMeaning
(probeMode, udpProbePort, pingSimultaneousConnectionLimit from above)
maxLocationsToReturnHow many answered locations reach the returned list. Not a search bound — the stop condition below is.
minNumberCandidatesMeasurementHow many locations must have answered before the search stops reaching further. Recommended 5.
minNumberGoodCandidatesHow 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.
searchBudgetMsWall-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.
probeSamplesProbes 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.
probeWindowMsCeiling 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.
probeTimeoutMsA probe with no answer by this point is a lost probe, not a slow one. Recommended 500.
performHttpDownloadTestRun the throughput phase at all.
performHttpUploadTestRecommended false: the ranking reads no upload figure.
bandwidthDurationModelfixed or auto. Recommended fixed — a ranking probe with a known cost per candidate.
bandwidthDurationMaxMsExact transfer length under fixed, ceiling under auto. Recommended 300.
downloadSizeRecommended large, so a fast location is not capped by the file running out.
simultaneousConnectionsRecommended 2 — fewer than the speed test's, because many locations are measured in one window.
info

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.

warning

fastMode and performHttpDownloadTest: false are not two spellings of the same thing.

  • fastMode is a parameter on the call, and it is minNumberGoodCandidates: 1 plus 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: false returns 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:

FieldWhat it tells you
probeModeRequested / probeModeResolvedWhich transport you asked for, and which one ran. auto → tcp means this network is on record as not answering UDP.
probeFallbackArmed / probeFallbackFiredfired 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 / tcpDownloadPortThe ports detection found on this network.
candidateBatches / candidatesMeasuredOne batch is normal; more means the near endpoints were not good enough and the search had to reach further.
selectionMs, unloadedLatencyMs, downloadMs, uploadMs, packetLossMs, totalMsWhere the time went.
warning

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​

FieldMeaning
(the four shared probe settings above)
tcpPingPortThe 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.
probeWindowMsHow long the sweep measures for. Named for what it is here — a ping sweep has no candidate phase to bound.

UdpProbeConfig — 1 field​

FieldMeaning
udpProbePortWhere the UDP echo service listens.
info

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.
warning

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.

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:

LanguageRecommended configuration
RustSpeedTestConfig::default()
Swift / KotlinSpeedTestConfig()
GoSpeedTestConfig{}
C# / Dartnew 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.

info

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 a csdk_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 matching csdk_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.

ConstructorStructRelease with
csdk_speedtest_recommended_configCSDKSpeedTestConfigcsdk_free_speedtest_config
csdk_speedtest_recommended_udp_probe_configCSDKUdpProbeConfigcsdk_free_udp_probe_config
csdk_instance_discovery_recommended_smart_location_search_configCSDKSmartLocationSearchConfigcsdk_free_smart_location_search_config
csdk_instance_discovery_recommended_ping_locations_configCSDKPingLocationsConfigcsdk_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.

probeModeWhat happens
AutoProbes 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.
UdpUDP, and only UDP. No detection, no fallback, nothing remembered. A network that blocks UDP yields no samples at all.
TcpTCP. Works everywhere; measures a fleet in waves rather than all at once.
unsetThe 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.

danger

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.

info

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). The probe_deadline_ms a 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.

info

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.

warning

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.

warning

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.

![](../../../.gitbook/assets/image (1).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​

NameTypeDescription
subscription_idStringID of the subscription which has a VPN entitlement. You can obtain that subscription id as described in Obtaining a VPN Subscription ID
callbackInstanceDiscoveryCallbackThis 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.

info

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().

danger

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​

NameTypeDescription
subscription_idStringID of the subscription which has a VPN entitlement. You can obtain that subscription id as described in Obtaining a VPN Subscription ID
configSpeedTestConfigConfiguration 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.
pub struct SpeedTestConfig {
/// How the endpoint to test against is chosen.
pub endpoint_selection_mode: SpeedTestEndpointSelectionMode,

/// How the transfer decides it is done. Default Auto.
pub bandwidth_duration_model: Option<BandwidthDurationModel>,
/// Ceiling on the HTTP up-/download transfer, in ms. Default 10000.
pub bandwidth_duration_max_ms: Option<u32>,
/// Whether to run the download / upload transfer. Default true for both.
pub perform_http_download_test: Option<bool>,
pub perform_http_upload_test: Option<bool>,
/// Ceiling on the candidate-search phase, in ms. Default 3000.
pub candidate_phase_max_ms: Option<u32>,
/// How many candidates must answer, and how many of those must be good, before the
/// search stops reaching further. Defaults 5 and 1.
pub min_number_candidates_measurement: Option<u16>,
pub min_number_good_candidates: Option<u16>,
/// The packet-loss phase (probe count, transport, pacing, ceiling) as one nested record.
/// Unset runs the recommended phase; 1000 probes.
pub packet_loss: Option<SpeedTestPacketLossConfig>,
/// The IPs of the VPN connection the device is on, which the run must not measure against.
/// Unset means disconnected.
pub ignore_endpoint_ips: Option<Vec<String>>,
/// Test file used by the transfers. Default Large (1 GB).
pub speedtest_download_size: Option<SpeedTestMaxDataTransferSizeByte>,
/// How long one latency probe waits for its answer, in ms. Default 1000.
/// Latency only — the packet-loss phase derives its own deadline.
pub latency_probe_timeout_ms: Option<u32>,
/// Parallel connections each transfer opens. Default 4.
pub speedtest_simultaneous_connections: Option<u32>,
}

pub enum SpeedTestEndpointSelectionMode {
/// This will pick an endpoint close to your current location (real or vpn location)
Automatic,
/// Prefer an endpoint in this location, falling back to the nearest measurable one
Preferred { location_id: String },
}
callbackInstanceDiscoveryCallbackThis 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.

warning

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​

NameTypeDescription
measurementsArray<MeasuredCandidate>The measurements to rank. For a protocol comparison, pass one MeasuredCandidate per protocol (same location), each tagged with its real PreferredProtocol.
configRankingConfig?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

NameTypeDescription
baselineSpeedtestBaseline?The unprotected line, for relative throughput scoring. null keeps the absolute curves.
lightway_recommendation_min_scoreFloat?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

NameTypeDescription
metricsClusterMetricsThe measurement taken with the VPN off — pass what create_speedtest_measurement returned, unmodified.
download_reference_cap_mbitFloat?The throughput at which more bandwidth stops being a quality difference, in Mbit/s. null uses 1000.
info

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

  1. Run the baseline measurement with Automatic (VPN off).
  2. Read the location the baseline was measured against from the result (clusterInstance.location.id).
  3. Connect the VPN to that location.
  4. Run the speed test with Preferred { locationId } from step 2 and ignoreEndpointIps set 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

  1. Run the baseline measurement with Preferred { locationId } from the location picker (VPN off).
  2. Connect the VPN to that location.
  3. Run the speed test with the same Preferred { locationId } and ignoreEndpointIps set 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.

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:

info

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 recommended changes. Every use-case list, score, category and suitable flag 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_score overrides 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 it null to 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 }
info

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​

NameTypeDescription
subscription_idStringID of a subscription with an AI entitlement. Must not be empty.
rankingRankingThe ranking to explain — pass the object rank_speedtest_measurements returned, unmodified. The label you gave each MeasuredCandidate is what the texts call it.
configRankingConfig?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.
modelString?ExpressAI model code. null uses the default ("oai").
languageExpressAiLanguage?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.

NameTypeDescription
glossaryArray<GlossaryEntry>The handful of measured things — responsiveness, steadiness, dropped data, speed — in plain words. Roughly one entry each; treat the count as variable.
baselineBaselineVerdict?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.
protocolsArray<ProtocolVerdict>One verdict per measured protocol. Same entries as the ranking, in the engine's overall order.
best_forBestForUseCaseThe winner of each use case, with the model's reason.
recommendedUseCaseWinnerThe engine's recommendation — the first entry of Ranking.recommended (see The recommended protocol). Its why is always empty — see the note below.

GlossaryEntry

NameTypeDescription
termStringPlain name with the technical word in brackets, e.g. "Responsiveness (latency)".
descriptionStringOne plain sentence on what it is.
importanceStringWhich 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.

NameTypeDescription
verdictStringTwo 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.
strengthsStringWhat the line does well.
weaknessesStringWhere the line itself is weak — the part every tunnel inherits — or the language's equivalent of "None worth noting."

ProtocolVerdict

NameTypeDescription
measurement_idUInt32Identifies the measurement this verdict is about — matches RankedCandidate.measurement_id in every list of the ranking.
labelStringThe app-given label of that measurement, copied from your MeasuredCandidate. Empty if you passed none.
verdictStringOne plain line on what this protocol is like to use.
strengthsStringOne or two sentences on what it does well.
weaknessesStringOne 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

NameTypeDescription
measurement_idUInt32The winning measurement — the first entry of that use case's list in the ranking. Computed by the SDK, not by the model.
labelStringThat measurement's app-given label.
whyStringA 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 holdsWhat you get back
Several measurements, or one over a real protocolA protocol comparison: one verdict per protocol, a winner named per use case.
Exactly one measurement tagged PreferredProtocol.noneA 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:

  • protocols holds exactly one entry — the connection. Its verdict is the headline: two or three sentences carrying both the state of the line and what it means for a VPN. strengths / weaknesses support it.
  • best_for is not about winning — nothing was competed against. Each why says 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.

warning

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:

MeasuredWhat the text may claim
Jitter, packet lossA 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.
LatencyThe 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.
ThroughputA 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.

warning

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.

info

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​

ErrorWhen
InvalidInputParametersEmpty subscription_id, or a baseline config passed with a ranking that holds only the unprotected line.
JsonDeserializationErrorThe model answered with something other than the requested JSON. Carries the raw text.
ExpressAiUnexpectedFinishThe completion stopped early (e.g. length), so the answer is incomplete.
ExpressAiProtocolThe 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​

NameTypeDescription
subscription_idStringID of the subscription with a VPN entitlement
configUdpProbeConfigWhich port to probe. Leave it at the recommended value unless your deployment moved the echo service.

Returns​

A UdpProbeReport.

NameTypeDescription
availableBoolWhether any probe came back at all. The answer, but not the whole answer — see below.
endpoints_probedUInt32Distinct endpoints probed.
probes_sentUInt32Datagrams sent, several per endpoint.
probes_answeredUInt32Datagrams that came back.
first_answer_msUInt32 (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.
info

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.

warning

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:

  1. Initially, you will receive several progress types labeled ping as the speed test identifies the optimal server. This process may take a few seconds.
  2. 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.
  3. Subsequently, you will receive multiple progress types labeled SpeedTestDownload/SpeedTestUpload, which will inform you of the current down- or upload speed and progress.