- Architecture
Voice latency on mobile browsers
Collect mobile voice timing without mixing clocks, then test autoplay, backgrounding, interruptions, and network transitions.
- Author
Udit Goenka- Editorial date
- Reading time
- 27 min

TL;DR: Measure mobile voice latency at the client, with audio state, page visibility, scheduling gaps, and transport observations recorded separately. Test foreground, background, interruption, and return-to-page cohorts on real devices; a server audio timestamp or desktop emulation does not establish what a mobile caller heard.
The server returns audio promptly. The phone remains silent until the user taps the screen, returns from another app, or changes the output route. A healthy server trace cannot explain that experience because the missing evidence sits on the client.
Start with the device boundary. Was audio allowed to start? Was the context running? Did the page remain active? Did packets arrive? Did the player schedule current audio, and did the output route make it audible? Each answer narrows the investigation without guessing that every pause is a model delay.
This guide provides a content-free browser timing collector, a tested statistics helper, and a real-device test matrix. It publishes no measured device ranking or performance claim for 60db Agent. The examples and policy limits are synthetic design fixtures; physical-device validation remains work you must perform for your supported product.
As checked on November 9, 2026, Chrome's Page Lifecycle documentation describes suspension and discarding under resource constraints, while MDN's audio-context state documentation describes interruption-related states. Support and behavior must still be checked on the actual browser and operating-system versions you ship.
Table of contents
- Define the mobile measurement boundary
- Separate audio readiness from network readiness
- Install a bounded client collector
- Build a real-device test matrix
- Diagnose pauses by evidence
- Release with explicit support boundaries
- Frequently asked questions
- Start with one reproducible mobile trace
Define the mobile measurement boundary
A mobile voice experience crosses capture, browser execution, transport, decoding, playback scheduling, and the physical audio route. Decide which of those boundaries your measurement reaches before collecting a latency number.
For caller-perceived response, the useful interval often begins with the end of caller speech and ends with audible assistant output. A browser can observe several intermediate events, but physical audibility requires a calibrated output observation if you intend to claim it directly.
The voice latency benchmark defines a reusable contract for those events. Apply it to the phone rather than substituting a server timestamp simply because that timestamp is easier to collect.
Keep acknowledgment and useful-answer boundaries separate. A short waiting clip can start while the substantive answer remains blocked. On a phone, it can also activate an output route or interact with user-gesture rules, making a changed conversational policy a confounder in the trial.
Keep clocks in their domains
Use performance.now() for local browser durations under its documented semantics. Keep server monotonic times, browser performance times, and audio-context time as separately named fields. They do not share a subtraction-ready origin merely because each value is numeric.
An audio context's current time describes its audio timeline. A timestamp from the main thread describes an observation by the page. Record both when useful, but do not subtract them directly to claim speaker delay without a justified mapping.
Cross-device comparisons need the same event definitions and cohort conditions. A phone with Bluetooth output and a phone using its speaker can have different audio paths. Label the route rather than attributing the whole difference to the browser engine.

Browser, audio-context, and server timestamps cannot be subtracted without a justified clock relationship.
Missing observations are data
A suspended or discarded page may stop producing telemetry. The absence of timer callbacks does not prove the network stopped, and the absence of a final event does not prove the call ended cleanly. Preserve incomplete attempts in the result set.
When the page returns, record a return event and a new observation epoch if the session or stream was replaced. Do not join a new peer connection's counters to an old connection's cumulative values. A reset must remain visible in both the trace and the aggregation.
Keep a server-side session record for business continuity where the application already has one. Client telemetry can explain the media experience, while the server or business service remains authoritative for tool outcomes. A phone leaving the page does not necessarily cancel an accepted operation.
Define the supported experience
A foreground web voice session and a call expected to continue indefinitely while the phone is locked are different product contracts. Write the intended behavior and supported limitations before testing. Do not promise background continuity because a desktop tab happened to keep playing.
The transport comparison helps identify which route is being tested. A media element, a Web Audio pipeline, and a WebRTC receiver can share a browser while exposing different activation, buffering, and lifecycle behavior.
If uninterrupted background calling is required, evaluate the actual platform capabilities against that requirement. The outcome can be a supported web path, a clearly explained limitation, or a different product surface. A keepalive timer is not proof that the operating system will preserve the audio session.
Separate audio readiness from network readiness
A connected socket or peer connection means transport setup reached its documented state. It does not establish that the browser has permission and readiness to produce sound. Treat transport and audio as independently observable paths.
The autoplay guide describes restrictions affecting media and Web Audio. Start or resume audio through the supported user interaction, handle rejection, and show the user the next required action instead of silently retrying in a loop.
A visible Start button should reflect real state. Disable duplicate starts while activation is pending, expose a useful failure message, and provide a stop control. Do not label the session ready before the relevant promise resolves and the application verifies the current state.
Resume does not prove audibility
AudioContext.resume() resumes a suspended audio context under its API contract and returns a promise. A resolved promise does not independently establish correct gain, a valid source, an audible device route, or successful playback of the current response.
Record context state changes and player-specific events. A context can be running while the source queue is empty, muted, disconnected, or stale. Verify the actual media path with a known synthetic fixture before attributing silence to a network or model component.
Avoid repeated recursive resume attempts. If the platform requires user interaction or an external interruption remains active, a tight retry loop consumes resources without supplying the missing condition. Provide an explicit recover action and record its outcome.
Visibility is an observation
The Page Visibility API exposes visibility changes and documents background behavior considerations. Visibility is useful cohort metadata. It is not a complete description of whether the operating system is executing the page or preserving its audio path.
Track page show and hide events where applicable, plus supported lifecycle events when your browser matrix requires them. Keep unsupported fields explicit. A browser that does not expose a freeze event still needs a tested return-to-page path.
Do not equate a long timer gap with a confirmed freeze. Main-thread work, background scheduling, system suspension, and diagnostic overhead can all delay a callback. Use the gap to locate a time window, then correlate other evidence before naming a cause.

Interruption and route changes
An incoming phone call, another audio application, a connected headset, or an operating-system action can affect the output path. Record the observed state and the tester's controlled action without claiming that one state transition proves every possible hardware event.
On return, check whether the original context and media session remain usable. If the application creates a new stream, invalidate old response epochs so delayed audio from before the interruption cannot play afterward. The interruption-safe agent guide supplies that ownership rule.
A recovery interface should tell the user what it can verify: audio resumed, connection being checked, or a new session required. Keep business state separate, especially if a tool operation continued while the device was interrupted.
Install a bounded client collector
The following module records content-free lifecycle observations and optional WebRTC audio statistics. It uses a bounded event buffer, serial sampling, feature checks, and explicit cleanup. It does not request a microphone, create a peer connection, or claim that a timer sample measures physical playback.
Save it as mobile-timing.mjs. Pass the audio context and peer connection your application already owns. The synthetic limits in the example are a collection budget, not a recommended sampling rate or a measured browser capability.
export function intervalBufferMs(previous, current) {
if (!previous || previous.id !== current.id) return null;
const values = [previous.delay, previous.count, current.delay, current.count];
if (!values.every(Number.isFinite)) return null;
const delay = current.delay - previous.delay;
const count = current.count - previous.count;
if (delay < 0 || count <= 0) return null;
return 1000 * delay / count;
}
export function createCollector({ context = null, peerConnection = null,
intervalMs = 1000, maxEvents = 512 } = {}) {
if (!Number.isFinite(intervalMs) || intervalMs <= 0 ||
!Number.isInteger(maxEvents) || maxEvents < 1) {
throw new Error("positive interval and event budget required");
}
const rows = [];
const previous = new Map();
const removers = [];
let timer = null, stopped = false, dropped = 0;
let lastTick = performance.now();
const record = (kind, fields = {}) => {
if (stopped) return;
if (rows.length === maxEvents) { rows.shift(); dropped++; }
rows.push({ atMs: performance.now(), kind,
visibility: document.visibilityState,
audioState: context?.state ?? "unavailable", ...fields });
};
const listen = (target, event, handler) => {
target.addEventListener(event, handler);
removers.push(() => target.removeEventListener(event, handler));
};
listen(document, "visibilitychange", () => record("visibility"));
listen(window, "pageshow", e => record("pageshow", { persisted: e.persisted }));
listen(window, "pagehide", e => record("pagehide", { persisted: e.persisted }));
if (context) listen(context, "statechange", () => record("audio-state"));
const sample = async () => {
if (stopped) return;
const now = performance.now();
record("sample", { tickGapMs: now - lastTick,
audioTimeSeconds: context?.currentTime ?? null });
lastTick = now;
try {
if (peerConnection?.getStats) {
const stats = await peerConnection.getStats();
if (stopped) return;
for (const stat of stats.values()) {
if (stat.type !== "inbound-rtp" || stat.kind !== "audio") continue;
const current = { id: stat.id, delay: stat.jitterBufferDelay,
count: stat.jitterBufferEmittedCount };
record("inbound-audio", {
receiverStatsId: stat.id,
jitterBufferDelay: stat.jitterBufferDelay ?? null,
jitterBufferEmittedCount: stat.jitterBufferEmittedCount ?? null,
intervalBufferMs: intervalBufferMs(previous.get(stat.id), current),
packetsReceived: stat.packetsReceived ?? null,
packetsLost: stat.packetsLost ?? null
});
previous.set(stat.id, current);
}
const activeIds = new Set([...stats.values()].map(s => s.id));
for (const id of previous.keys()) if (!activeIds.has(id)) previous.delete(id);
}
} catch {
record("stats-error");
} finally {
if (!stopped) timer = setTimeout(sample, intervalMs);
}
};
record("collector-start");
timer = setTimeout(sample, intervalMs);
return {
mark: record,
snapshot: () => ({ rows: rows.map(row => ({ ...row })), dropped }),
stop() {
if (stopped) return;
record("collector-stop");
stopped = true;
clearTimeout(timer);
for (const remove of removers) remove();
previous.clear();
}
};
}
The collector records each event's current visibility and audio state. It keeps local performance time separate from audio time. No field subtracts server timestamps or claims to observe the physical speaker.
An optional peer connection supplies statistics through getStats(). Missing fields remain null through the helper rather than becoming zero delay. Missing support is a measurement limitation, not evidence of perfect playback.
Understand the jitter-buffer helper
The WebRTC statistics specification defines cumulative jitter-buffer delay and emitted-count fields. The helper uses their deltas for an interval mean when both observations refer to the same stats object and the counters advance validly.
The delay counter is in seconds, so the result is converted to milliseconds. A reset, changed identity, missing value, or nonpositive emitted-count delta produces null. Do not average those nulls as zero, and do not reinterpret this interval mean as a percentile of individual samples.
Every saved inbound row retains receiverStatsId, the original cumulative jitterBufferDelay in seconds, and jitterBufferEmittedCount as a sample count. Keep these raw counters beside the computed interval mean so another engineer can reproduce the calculation and weight compatible intervals by their emitted-count deltas. Missing counters remain null; a reported zero remains zero.
The stats identifier belongs to a receiver statistics object within the current peer connection and trace. It is not a caller identity or a globally unique device identifier. Keep the owning connection or observation epoch in the enclosing trace, group rows by that scope and stats identifier, and start a new baseline when identity changes or counters reset. Never calculate packet or jitter-buffer deltas across those boundaries.
The value describes time in the receiver jitter buffer under the statistic's definition. It excludes other parts of end-to-end response latency, including earlier model work and the full physical output route. Keep its name specific.
Run the synthetic accounting and collector tests
Save this as mobile-timing-test.mjs beside the module, then run node mobile-timing-test.mjs. These fixtures test units, reset handling, missing observations, and identity changes. They also execute the complete collector with deliberately scoped browser-event and timer stubs, checking two synthetic receivers and reconstruction from saved counters. They do not validate a browser, device, network, or physical audio path.
import assert from "node:assert/strict";
import { intervalBufferMs, createCollector } from "./mobile-timing.mjs";
const first = { id: "synthetic-a", delay: 1, count: 100 };
assert.ok(Math.abs(intervalBufferMs(first,
{ id: "synthetic-a", delay: 1.2, count: 120 }) - 10) < 1e-9);
assert.equal(intervalBufferMs(null, first), null);
assert.equal(intervalBufferMs(first,
{ id: "synthetic-b", delay: 2, count: 200 }), null);
assert.equal(intervalBufferMs(first,
{ id: "synthetic-a", delay: 0, count: 1 }), null);
assert.equal(intervalBufferMs(first,
{ id: "synthetic-a", delay: 2, count: 100 }), null);
assert.equal(intervalBufferMs(first,
{ id: "synthetic-a", delay: undefined, count: 120 }), null);
// Synthetic browser surfaces and manual timers, not a device simulation.
const names = ["document", "window", "setTimeout", "clearTimeout"];
const originals = new Map(names.map(name =>
[name, Object.getOwnPropertyDescriptor(globalThis, name)]));
const timers = new Map();
let nextTimer = 0;
let collector;
try {
const documentStub = new EventTarget();
documentStub.visibilityState = "visible";
const stubs = {
document: documentStub,
window: new EventTarget(),
setTimeout: callback => {
const id = ++nextTimer;
timers.set(id, callback);
return id;
},
clearTimeout: id => timers.delete(id)
};
for (const name of names) Object.defineProperty(globalThis, name,
{ configurable: true, writable: true, value: stubs[name] });
const audio = (id, delay, count, packets) => ({
id, type: "inbound-rtp", kind: "audio",
jitterBufferDelay: delay, jitterBufferEmittedCount: count,
packetsReceived: packets, packetsLost: 0
});
const snapshots = [
[audio("receiver-a", 1, 100, 50), audio("receiver-b", 4, 200, 100)],
[audio("receiver-a", 1.2, 120, 54), audio("receiver-b", 4.6, 230, 110)],
[audio("receiver-a", 0, 0, 0), audio("receiver-b", undefined, undefined, 111)]
];
const peerConnection = { async getStats() {
const next = snapshots.shift();
assert.ok(next, "unexpected synthetic sample");
return new Map(next.map(stat => [stat.id, stat]));
} };
collector = createCollector({ peerConnection, maxEvents: 32 });
const runNext = async () => {
const [id, callback] = timers.entries().next().value;
timers.delete(id);
await callback();
};
await runNext();
await runNext();
const rows = collector.snapshot().rows.filter(row => row.kind === "inbound-audio");
assert.deepEqual(rows.map(row => row.receiverStatsId),
["receiver-a", "receiver-b", "receiver-a", "receiver-b"]);
const asCounter = row => ({ id: row.receiverStatsId,
delay: row.jitterBufferDelay, count: row.jitterBufferEmittedCount });
const intervals = [[rows[0], rows[2]], [rows[1], rows[3]]];
let delayDelta = 0, countDelta = 0;
for (const [before, after] of intervals) {
assert.equal(after.intervalBufferMs,
intervalBufferMs(asCounter(before), asCounter(after)));
delayDelta += after.jitterBufferDelay - before.jitterBufferDelay;
countDelta += after.jitterBufferEmittedCount - before.jitterBufferEmittedCount;
}
assert.ok(Math.abs(1000 * delayDelta / countDelta - 16) < 1e-9);
assert.equal(rows[2].packetsReceived - rows[0].packetsReceived, 4);
assert.equal(rows[3].packetsReceived - rows[1].packetsReceived, 10);
await runNext();
const last = collector.snapshot().rows.filter(row => row.kind === "inbound-audio").slice(-2);
assert.equal(last[0].jitterBufferDelay, 0);
assert.equal(last[0].jitterBufferEmittedCount, 0);
assert.equal(last[0].intervalBufferMs, null);
assert.equal(last[1].jitterBufferDelay, null);
assert.equal(last[1].jitterBufferEmittedCount, null);
assert.equal(last[1].intervalBufferMs, null);
collector.stop();
assert.equal(timers.size, 0);
const stopped = collector.snapshot();
documentStub.dispatchEvent(new Event("visibilitychange"));
assert.deepEqual(collector.snapshot(), stopped);
} finally {
collector?.stop();
for (const name of names) {
const descriptor = originals.get(name);
if (descriptor) Object.defineProperty(globalThis, name, descriptor);
else delete globalThis[name];
}
}
console.log("Synthetic accounting and collector fixtures passed");
The tolerance in the first assertion handles floating-point representation in the synthetic arithmetic. It is an arithmetic-test tolerance, not a claim of timing precision or measurement uncertainty on a real device. Keep those two concepts separate in the report.
Integrate with an existing session
Create one collector when the supported session starts. Mark application-owned boundaries such as user gesture, connection ready, input speech ended under your detector, first response received, and playback scheduled. Give every boundary a documented owner and clock.
Call stop() when the session ends, and save an approved snapshot before discarding the object. The collector owns only its listeners and timer. It does not close the application's audio context or peer connection, because those resources can have a broader owner.
Avoid starting another collector on every component render or reconnect callback. Reuse or stop the existing instance, then create a new observation epoch if the underlying session changed. Duplicate observers increase overhead and can produce conflicting event histories.
The observability guide explains how to join content-free events with a server trace. Use opaque session correlation under your data policy, and do not put raw audio, transcripts, IP addresses, or tool payloads into the collector merely because they are available nearby.
Build a real-device test matrix
A useful matrix varies the conditions that can change the browser audio path. Start with the operating systems, browser versions, hardware classes, and output routes your product explicitly supports. Add lifecycle and network transitions as controlled actions.
Record exact tested versions and dates. A statement such as "works on mobile" is too broad to reproduce. A device matrix should say what was tested, what passed the acceptance criteria, and which cells remain untested.
Use desktop emulation for layout and early smoke checks, then physical devices for audio, interruptions, resource pressure, and route behavior. A narrow viewport cannot reproduce the phone's operating-system lifecycle or audio hardware.
Lifecycle cohorts
Keep foreground steady-state, background transition, background interval, and return-to-page recovery separate. A pooled percentile can look stable while a minority of return attempts remain silent or require another user gesture.
Control the tester action. For example, begin a synthetic call in the foreground, switch to another application, return, and attempt the next turn. Record what the tester did and what the collector observed, while leaving the duration as a documented test input rather than a claim about platform limits.
Include page reload and process replacement where supported by the test environment. The collector's in-memory rows may disappear when the page is discarded, so an incomplete trace is expected evidence. Persist only the approved small state needed for recovery, using the application's existing mechanism.
Audio-route cohorts
Test speaker, wired output where relevant, and supported wireless output separately. Record mute, volume, and route selection under a repeatable procedure. A silent test with volume down is a test setup failure, not a transport benchmark result.
Include route changes during a response and during a pause. Observe whether the application detects a change, whether playback continues, and whether stale queued audio appears after recovery. Keep physical audibility checks separate from software state assertions.
Use a synthetic signal or approved phrase that makes onset and completion easy to identify. Avoid real customer content for hardware bring-up. When human speech is needed for representative listening, apply consent and retention rules before collecting it.
Network and resource cohorts
Test the supported network routes with controlled disruptions, and distinguish a network change from a page lifecycle change. A Wi-Fi-to-cellular transition can coincide with backgrounding, so annotate both rather than assigning the entire delay to one cause.
Keep diagnostic tooling and load consistent between candidates. Remote debugging, heavy console logging, screen recording, and battery-saving settings can affect the environment. Record which were active and run a lighter-observer check before treating a trace as representative.
Exercise realistic resource pressure without damaging or destabilizing the tester's device. Use the team's approved device-lab procedures. The goal is a reproducible cohort, not an uncontrolled stress experiment that cannot be repeated safely.
Comparison criteria
A check means the method directly observes the criterion under its stated setup. A cross means additional evidence is required. These methods complement one another and do not rank browser products.
| Method | Local lifecycle | Transport counters | Scheduled playback | Physical audibility |
|---|---|---|---|---|
| Content-free browser collector | ✓ | ✓ when available | ✗ without player marks | ✗ |
| Player-owned scheduling marks | ✗ | ✗ | ✓ | ✗ |
| Calibrated output capture | ✗ | ✗ | ✗ | ✓ |
| Desktop viewport emulation | ✗ | ✗ | ✗ | ✗ |
The collector row requires an actual peer connection for transport counters. A collector running without one still records lifecycle and context state, but it cannot invent transport evidence. Keep capability availability in the result rather than silently filling missing columns.

Diagnose pauses by evidence
Begin with a single failed attempt and reconstruct its local sequence. Look for audio readiness, visibility changes, callback gaps, received counters, scheduling marks, and the tester's output observation. Name the first boundary where evidence becomes missing or inconsistent.
If packets arrive while the context is suspended, investigate activation or interruption before blaming the server. If the context runs and the player queue remains empty, inspect decoding and admission. If playback is scheduled but physical output is absent, investigate gain and route evidence.
These are diagnostic branches, not automatic root-cause claims. Several conditions can overlap, and a state observation may occur after the initiating event. Preserve uncertainty until a controlled reproduction distinguishes the alternatives.
Scheduling gaps
A large timer gap indicates delayed execution of the observer. It does not quantify missing audio directly. Audio work can proceed in another execution context while the main thread is delayed, or the whole page can be suspended.
Correlate the gap with context state, media counters, and player continuity. Keep the collection interval out of the media scheduler. A telemetry timer should observe the application, not become the clock that determines when every audio sample plays.
The audio chunk guide explains why local and downstream queue age matter. Reducing application chunk duration cannot repair an activation restriction or guarantee execution of a background page.

A delayed observer callback needs audio and lifecycle evidence before it can explain a dropout.
Stale playback after return
A returning page can receive audio generated for an earlier turn. Check response epoch before admitting it to playback. Reconnection is not permission to play everything that accumulated while the user was away.
If the application resumes the same valid conversation, reconcile current task state before speaking. A tool result can remain valid while an old waiting message has become obsolete. The truthful waiting-speech guide separates those outcomes.
Do not blindly replay microphone input after an interruption either. Recognition stream identity, media timestamps, and turn state need a supported continuation contract. The partial-transcript guide explains why old recognition events cannot become commands in a new stream.
Counter interpretation
Use deltas for cumulative counters within a stable stats identity, and retain raw snapshot values for recomputation. A changed or reset counter should begin a new interval rather than producing a negative loss or delay measurement.
A lifetime mean can conceal a recent burst of buffering. The interval helper improves locality, but it still produces a mean over emitted samples in the interval. Report the denominator and avoid turning a handful of interval means into a claim about individual-sample percentiles.
Missing counters are common enough to deserve a first-class availability field. A null jitter-buffer value can mean no prior snapshot, no emitted samples, unsupported statistics, or a reset. Preserve the reason where it changes diagnosis instead of merging every null into the same success bucket.

Only compatible snapshots with valid advancing counters support an interval jitter-buffer mean.
Release with explicit support boundaries
Define acceptance criteria for initial activation, foreground turns, interruption, return, and session shutdown. Include failures and incomplete attempts alongside latency summaries. A fast successful subset does not establish a reliable mobile experience.
Require that old response audio stays invalid after session replacement, that the user can recover when activation is blocked, and that diagnostic collectors stop cleanly. These are observable behaviors suitable for a regression suite even when physical latency requires a device lab.
The voice regression guide supplies the release structure. Keep deterministic helper tests, browser integration checks, and physical-device listening results in separate categories so each claim has the right proof.
Communicate limitations in the product
If background continuation is unsupported or conditional, explain the supported foreground behavior before the caller relies on it. A clear recovery button and preserved task state are more useful than silent retries that leave the user guessing whether the agent heard them.
Do not expose implementation jargon in the user flow unless it helps the caller act. "Tap to resume audio" can be useful. A raw AudioContext state or internal transport error code usually belongs in diagnostics rather than the primary conversation.
Keep business outcome wording truthful during recovery. A disconnected browser does not establish that a booking failed. Reconcile the authoritative operation and communicate the verified state, applying the same contract as any other interrupted call.
Write a repeatable device procedure
Give testers a short procedure that names the starting state and the recovery observation. Begin from the supported browser with a clean test session, a known output route, and a documented volume setting. Use the same synthetic prompt and response fixture for the paired attempts so content length does not change the work.
For initial activation, record the user gesture, the activation promise outcome, the first valid source admission, and the chosen playback observation. A tester should distinguish a permission prompt from a silent failure. Do not start the media timer after the user has already spent time resolving an activation problem if the report claims a startup experience.
For a background test, document the exact action: switching tabs, switching apps, locking the screen, or another supported transition. These actions are different cohorts. Record the return action and whether another tap was required before the next valid response became audible under the test's observation method.
For an interruption test, use an approved controlled interruption and keep its identity in the test notes. Record whether the application retained its session, resumed the context, replaced the stream, or required a fresh start. Avoid a generic pass result when recovery used a different path from the one the product promises.
For a route-change test, verify the initial route before the change and the final route afterward. Mark which response was in progress. A transition during silence can pass while a transition during queued output clips words or releases stale speech, so both positions deserve a fixture when route changes are supported.
Repeat the procedure with the same observer configuration. If a failure cannot be reproduced, preserve the incomplete evidence and the uncertainty. An intermittent failure still belongs in the attempt denominator, while a claimed cause requires more than one suggestive correlation.
Budget the observer separately from media
The example's event cap bounds diagnostic memory by dropping the oldest event and increasing a counter. That policy protects the session from unbounded telemetry, but it can remove the beginning of a long attempt. A snapshot with dropped events is an incomplete trace and must be labeled accordingly.
Choose a session-length budget based on the test procedure and expected event rate. If the procedure can exceed that budget, save approved intermediate snapshots or use the application's existing bounded telemetry pipeline. Do not increase the cap indefinitely or send raw payloads merely to avoid thinking about retention.
The sample uses an array because its small bounded scope is easy to inspect. If measurement proves array shifting affects the target device, reuse the project's existing ring buffer or replace the small queue under a focused test. A new general telemetry abstraction is unnecessary until the actual observer exposes a problem.
Measure with and without optional diagnostics. A debug build that logs every event, renders a chart, and records the screen can change scheduling. The useful comparison keeps the observer stable or states its differences, then checks whether the lighter collection path changes the conclusion.
Never use the collector's sample timer to pace audio. It is allowed to run late because it observes state. Audio scheduling belongs to the media path and its appropriate execution context. Combining the two makes a delayed diagnostic callback become the cause of the dropout it was meant to explain.
Preserve denominators across recovery
A page reload creates a new document and can erase the previous collector. Retain an approved attempt identifier outside the transient rows when the test harness supports it, so the incomplete attempt remains countable. A new session should not erase evidence that the previous one failed to recover.
Define success separately for media recovery and task recovery. The audio can resume while the business state remains unknown, or the business action can complete while the caller never hears the result. Report both outcomes rather than allowing one success flag to represent incompatible claims.
Keep startup failures, mid-session interruptions, incomplete traces, missing stats, and confirmed playback failures in distinct categories. These are different denominators for diagnosis. A release summary can present them together, but it should retain enough detail to reconstruct the population behind every timing statistic.
A supported-device cohort also needs a sample count and a stated test scope. One successful attempt proves that the path worked once under those conditions. It does not establish a stable population percentile or broad platform support without the rest of the planned evidence.
Change one owner at a time
After locating a suspicious boundary, design a paired intervention that changes only its owner. If activation is the problem, keep the network and response fixture fixed while testing the supported user-gesture path. If queue admission is the problem, keep activation and the source audio fixed while testing the queue repair.
A network intervention should preserve the same lifecycle action. Otherwise a successful trial can result from keeping the page in the foreground rather than improving network recovery. Similarly, a playback change that also replaces the transport cannot identify which change resolved the pause.
Write the expected observation before running the intervention. An activation repair should change readiness and playback evidence without requiring a different business outcome. A queue repair should reduce obsolete waiting or gaps under the same source sequence. The experiment needs a failure category that can disprove your explanation.
If the expected observation does not occur, preserve the negative result and return to the trace. Avoid adding another retry loop solely because the first one did not help. A failed controlled intervention is useful evidence that the suspected owner was incomplete or wrong.
When several owners are involved, stage the repairs and rerun the same fixture after each one. Keep the final acceptance criteria across the whole path so a local improvement cannot hide a new problem elsewhere, such as stale output after recovery or lost caller input.
Retain a reproducible report
Save device and browser versions, output route, lifecycle action, network cohort, observer configuration, attempt counts, and failure categories. Include the exact collector and player versions so another engineer can reproduce the boundary definitions.
State which measurements are software observations and which include a physical output capture. Document calibration and uncertainty for the latter. If no physical measurement exists, label the finish boundary as scheduled playback or another directly observed event.
Compare the same cohorts after deployment using approved content-free telemetry. A changed browser release, device mix, or network route can invalidate an earlier setting. Keep a rollback path and a supported user recovery route instead of assuming a server-side improvement covers every mobile failure.
Frequently asked questions
Why is voice fast on desktop but slow on my phone?
The phone adds different lifecycle, audio activation, resource, output-route, and network conditions. Collect client evidence before assigning the delay to the model or server. A desktop result with a narrow viewport does not reproduce the operating system or audio hardware of a physical mobile device.
Does a connected WebRTC session prove audio works?
It proves only the transport state under the API contract. Audio can still be blocked, suspended, muted, incorrectly routed, undecoded, or absent from the player queue. Observe readiness and playback separately, then use a physical output check for claims about what the caller heard.
Should I start audio from a user gesture?
Use the supported interaction required by your target browser and handle the relevant promises and failures. Provide a visible recover action if activation is blocked. Repeated automatic attempts cannot replace a required user interaction or resolve an external hardware interruption.
What does AudioContext running mean?
It describes the context's operational state. It does not prove a valid source is connected, the gain is audible, the current response is scheduled, or the output route reaches the listener. Keep state observation and physical audibility as separate evidence.
Can I promise background voice in a web page?
Only within a tested, documented support contract for your target platforms. Browsers and operating systems can change execution and audio behavior under backgrounding or resource pressure. A successful foreground test or a keepalive timer does not establish indefinite locked-screen continuity.
Is page visibility the same as page execution?
No. Visibility is one observable property, while freezing, discarding, and operating-system suspension involve broader lifecycle behavior. Record visibility and available lifecycle events, then test the return path on real devices. Keep unsupported observations explicit.
What does a long timer gap indicate?
It indicates that the callback ran later than the previous local observation interval. Main-thread work, scheduling policy, suspension, or measurement overhead can cause it. Correlate context and media evidence before labeling the gap as missing audio or a confirmed page freeze.
Can performance.now be subtracted from server time?
Not without a justified clock relationship. Browser and server monotonic clocks have different origins. Calculate local durations and join event identities, or use a documented synchronization method with uncertainty when a cross-machine one-way estimate is required.
What does jitter-buffer delay measure?
Under the WebRTC statistics definition, it describes accumulated residence in the receiver jitter buffer. Dividing valid interval deltas by the corresponding emitted-count delta gives an interval mean. It does not represent the entire speech-to-playback path or physical speaker latency.
Why does the helper return null?
It lacks a valid comparable interval when observations are missing, identities differ, counters reset, or no samples were emitted. Null is missing evidence, not zero delay. Preserve that distinction when aggregating results and diagnosing unsupported statistics.
Is an interval mean a percentile?
No. It summarizes the emitted samples represented by that interval's counters. A percentile requires an appropriate distribution of observations with a defined population. Do not label a collection of interval means as individual-sample latency without changing and explaining the measurement boundary.
Can desktop emulation replace device testing?
It can help with layout and early application checks. It cannot reproduce the phone's audio hardware, operating-system interruptions, background lifecycle, or route changes. Use physical devices for claims that depend on those boundaries.
How should I test wireless audio?
Use the supported headset and route under a documented setup, then test initial playback and route changes during the session. Keep route cohorts separate from speaker tests. Software scheduling marks still need an output observation if you want to measure what the listener hears.
Should I replay queued speech after returning?
Only if it remains valid for the current response epoch and task state under your continuation contract. Old waiting clips and canceled answers should stay invalid. Reconcile the current operation before speaking rather than draining every byte accumulated during absence.
What if the page is discarded before logs are saved?
Count the attempt as incomplete and preserve approved recovery state through the application's existing mechanism. In-memory telemetry can disappear with the page. Do not invent a clean shutdown event or remove the attempt from the denominator because its final trace is missing.
Does the collector record conversations?
The example records content-free state, local timing, and optional counters. It requests no microphone and stores no transcripts or audio. Keep that boundary when integrating it, and apply approved access and retention to session correlation and device metadata.
How do I prevent duplicate collectors?
Give the session one explicit collector owner. Reuse or stop it before replacement, and call stop when its task ends. Component rerenders and reconnect callbacks should not create independent timers and listeners that survive after the original session is gone.
What should a mobile release report include?
Include tested versions, devices, output routes, lifecycle and network cohorts, observer configuration, attempt counts, failures, and explicit measurement boundaries. State which cells remain untested. Keep software timing and physical output measurements separate so readers can assess the actual evidence.
What do the code tests prove?
They verify the helper's arithmetic and execute the full collector with scoped synthetic browser surfaces and manual timers. Saved rows distinguish two receivers, reproduce interval and weighted means, preserve null availability, and expose resets. They do not establish real-browser or physical-device audio behavior. Those require browser integration and real-device tests under the stated matrix.
How should I evaluate 60db Agent on mobile?
Use the documented integration and the same client-side contract described here. Test your supported devices, browser versions, output routes, lifecycle transitions, and business recovery paths. This article supplies a generic method and reports no private implementation or measured mobile performance for 60db Agent.
Start with one reproducible mobile trace
Install the bounded collector around an existing test session, run its synthetic accounting and collector tests, and reproduce one foreground and one return-to-page case on a real phone. Join the client observations with server events while keeping clock domains and business outcomes separate.
Add the resulting fixture and support boundary to your regression plan. Expand the device matrix only after the trace can explain the first failure without guessing which component, lifecycle transition, or audio route owned the missing delay.
