Overview
The SpeedTest engine communicates state changes through five assignable callback properties. Assign a function to any of these properties on your engine instance to receive notifications.
const engine = new SpeedTest({ autoStart: false });
engine.onRunningChange = (running) => { /* ... */ };
engine.onResultsChange = ({ type }) => { /* ... */ };
engine.onPhaseChange = ({ measurement, measurementId }) => { /* ... */ };
engine.onFinish = (results) => { /* ... */ };
engine.onError = (error) => { /* ... */ };
engine.play();
Callbacks can be assigned before or after calling play(). If you assign onFinish after measurements have already completed, it will not be called retroactively — assign callbacks before starting for reliable delivery.
onResultsChange fires many times throughout a test run (once per
individual data point). Use it for live UI updates. Use onFinish for
final result display — it fires exactly once when all measurements complete.
onRunningChange
onRunningChange: (running: boolean) => void
Fires whenever the engine transitions between running and not-running states.
When it fires:
- When
play() successfully starts or resumes the engine → running: true
- When
pause() is called → running: false
- When the last measurement in the sequence completes →
running: false
- When
restart() clears the engine (briefly false before the new run starts true)
Arguments:
The new value of engine.isRunning after the transition.
Example: show a loading spinner
const spinner = document.getElementById('spinner')!;
const statusText = document.getElementById('status')!;
engine.onRunningChange = (running: boolean) => {
spinner.style.display = running ? 'block' : 'none';
statusText.textContent = running ? 'Test in progress…' : 'Test paused';
};
onResultsChange
onResultsChange: ({ type: string }) => void
Fires whenever any measurement result is updated. This is the primary event for building live dashboards — it fires on every individual data point as it arrives.
When it fires:
- At the start of each measurement step (initial state update)
- After every individual latency probe completes
- After every individual download or upload request completes
- After every packet received during a packet loss measurement
- When a measurement step finishes
Arguments:
The category of measurement that changed. One of:
"latency" — unloaded latency probe completed
"download" — download bandwidth measurement updated
"upload" — upload bandwidth measurement updated
"packetLoss" — packet loss measurement updated
Example: live dashboard
engine.onResultsChange = ({ type }: { type: string }) => {
const results = engine.results;
switch (type) {
case 'latency': {
const latency = results.getUnloadedLatency();
const jitter = results.getUnloadedJitter();
if (latency !== undefined) {
document.getElementById('latency')!.textContent = `${latency.toFixed(1)} ms`;
}
if (jitter !== undefined) {
document.getElementById('jitter')!.textContent = `${jitter.toFixed(1)} ms`;
}
break;
}
case 'download': {
const bw = results.getDownloadBandwidth();
if (bw !== undefined) {
document.getElementById('download')!.textContent =
`${(bw / 1e6).toFixed(1)} Mbps`;
}
break;
}
case 'upload': {
const bw = results.getUploadBandwidth();
if (bw !== undefined) {
document.getElementById('upload')!.textContent =
`${(bw / 1e6).toFixed(1)} Mbps`;
}
break;
}
case 'packetLoss': {
const loss = results.getPacketLoss();
if (loss !== undefined) {
document.getElementById('packetLoss')!.textContent =
`${(loss * 100).toFixed(2)}%`;
}
break;
}
}
};
results.getDownloadBandwidth() (and similar methods) return undefined
until enough data has been collected. Always guard with an !== undefined
check before rendering values.
onPhaseChange
onPhaseChange: ({ measurement: MeasurementConfig, measurementId: number }) => void
Fires each time the engine advances to the next measurement in the sequence.
When it fires:
- Once per entry in the
measurements array, in order, as the engine starts each step
Arguments:
measurement
MeasurementConfig
required
The configuration object for the measurement that is about to begin. This is
the same object that was provided in the measurements array (e.g.
{ type: 'download', bytes: 1000000, count: 8 }).
The zero-based index of this measurement within the measurements array.
Useful for computing a progress percentage.
Example: progress indicator
import SpeedTest from '@cloudflare/speedtest';
import type { MeasurementConfig } from '@cloudflare/speedtest';
const engine = new SpeedTest({ autoStart: false });
const progressBar = document.getElementById('progress')! as HTMLProgressElement;
const phaseLabel = document.getElementById('phase')!;
// Default config has 15 measurement steps
const TOTAL_STEPS = 15;
engine.onPhaseChange = ({
measurement,
measurementId,
}: {
measurement: MeasurementConfig;
measurementId: number;
}) => {
progressBar.value = measurementId;
progressBar.max = TOTAL_STEPS;
phaseLabel.textContent = `Step ${measurementId + 1} of ${TOTAL_STEPS}: ${measurement.type}`;
};
engine.play();
onFinish
onFinish: (results: Results) => void
Fires exactly once when all measurements in the sequence complete. At this point every result method on the Results object returns its final value.
When it fires:
- After the last measurement in the
measurements array finishes
- The engine sets
isRunning to false and isFinished to true before this callback is invoked
Arguments:
The final Results object. All methods return their fully computed values.
This is the same object accessible via engine.results.
Example: final summary and scores
engine.onFinish = (results) => {
const summary = results.getSummary();
const scores = results.getScores();
console.log('Summary:', {
download: summary.download
? `${(summary.download / 1e6).toFixed(1)} Mbps`
: 'N/A',
upload: summary.upload
? `${(summary.upload / 1e6).toFixed(1)} Mbps`
: 'N/A',
latency: summary.latency ? `${summary.latency.toFixed(1)} ms` : 'N/A',
jitter: summary.jitter ? `${summary.jitter.toFixed(1)} ms` : 'N/A',
packetLoss: summary.packetLoss !== undefined
? `${(summary.packetLoss * 100).toFixed(2)}%`
: 'N/A',
});
// AIM scores classify connection quality per use case
for (const [useCase, score] of Object.entries(scores)) {
console.log(`${useCase}: ${score.classificationName} (${score.points} pts)`);
}
document.getElementById('results-panel')!.style.display = 'block';
};
onFinish is invoked asynchronously via setTimeout with a delay of 0
after the last measurement completes. This ensures the isFinished getter
is already true by the time your callback runs.
onError
onError: (error: string) => void
Fires when a measurement fails due to a network or configuration error. The engine logs the error and continues to the next measurement rather than stopping the entire test.
When it fires:
- Connection error during a latency measurement
- Connection error during a download measurement
- Connection error during an upload measurement
- Connection error when connecting to the TURN server for packet loss
- Failure to obtain TURN server credentials
Arguments:
A human-readable description of the error, including the measurement type
and the underlying cause. Example:"Connection error while measuring download: Failed to fetch"
"Error while measuring packet loss: unable to get turn server credentials."
Example: error toast
engine.onError = (error: string) => {
console.error('SpeedTest error:', error);
const toast = document.createElement('div');
toast.className = 'toast toast-error';
toast.textContent = `Measurement error: ${error}`;
document.body.appendChild(toast);
setTimeout(() => toast.remove(), 5000);
};
All events wired together
The following example shows all five callbacks configured on a single engine instance.
import SpeedTest from '@cloudflare/speedtest';
import type { MeasurementConfig, Results } from '@cloudflare/speedtest';
const engine = new SpeedTest({ autoStart: false });
engine.onRunningChange = (running: boolean) => {
console.log('[running]', running);
};
engine.onPhaseChange = ({
measurement,
measurementId,
}: {
measurement: MeasurementConfig;
measurementId: number;
}) => {
console.log(`[phase] ${measurementId}: ${measurement.type}`);
};
engine.onResultsChange = ({ type }: { type: string }) => {
// Fires many times — keep this handler fast
const r = engine.results;
if (type === 'download' && r.getDownloadBandwidth() !== undefined) {
console.log(`[download] ${((r.getDownloadBandwidth()!) / 1e6).toFixed(2)} Mbps`);
}
};
engine.onError = (error: string) => {
console.warn('[error]', error);
// Engine continues to next measurement automatically
};
engine.onFinish = (results: Results) => {
// Fires once — safe to do more expensive work here
console.log('[finish]', results.getSummary());
console.log('[scores]', results.getScores());
};
engine.play();
onResultsChange vs onFinish
| onResultsChange | onFinish |
|---|
| Fires | Many times during the test | Once, at completion |
| Timing | After each individual data point | After all measurements finish |
| Results state | Partial — values may be undefined | Final — all values available |
| Use for | Live progress UI, streaming updates | Final summary, AIM scores, reporting |
| Performance | Keep handler fast; it runs frequently | Can do heavier work here |