← All articles

Voice AI engineering

Instrument voice-agent latency as a trace, not a stopwatch

Trace voice-agent latency across capture, endpointing, transcription, model work, tools, synthesis, playback, and interruption recovery on live phone calls.

A distributed voice call trace shows timed spans for speech, models, tools, synthesis, playback, and barge-in

Take it with you

CSVFree and ungated

Voice latency event schema

A CSV event dictionary for timestamps, spans, attributes, derived metrics, privacy handling, and latency ownership.

voice-latency-event-schema.csv

Download CSV

The dashboard says “response latency: 1.4 seconds.” Which 1.4 seconds?

Was the caller still speaking? Did endpointing wait? Did the transcript churn? Did the model stall? Did a CRM call sit in another region? Did text-to-speech buffer a number? Did audio reach the phone late?

If you own voice reliability, one stopwatch gives you a complaint without a culprit. This tutorial shows how to build a call trace with spans, point events, and derived conversation metrics. The payoff is simple: when a caller says the agent felt slow, you can see where the time went.

Voice latency is a distributed-systems problem

A June 2026 r/AI_Agents post listed the chain builders end up debugging: media capture, transport, VAD, speech recognition, endpointing, model response, tool calls, synthesis, playback, and interruption handling. The thread is practitioner chatter rather than a benchmark, but the decomposition is sound. OpenAI’s account of delivering low-latency voice AI at scale likewise describes latency as a full-stack concern involving network paths, inference, audio streaming, caching, and service design. Your stack may use different providers, but the caller still experiences the sum.

Scroll diagram horizontally on smaller screens.

A voice call trace separates duration spans from point events and maps each wait to the team that owns it

Pick one clock and one trace ID

Choosing one clock and one trace ID sounds boring, but it prevents hours of false debugging. Every event in one call needs:

  • A stable conversation_id.
  • A turn_id.
  • A monotonic timestamp for local duration math.
  • A wall-clock timestamp for cross-service correlation.
  • Trace and span context propagated across your media, orchestration, model, and tool services.

Clock skew can make a child event appear before its parent. Prefer durations measured inside a service, then use trace correlation to join them. If separate machines must share absolute timing, monitor their clock synchronization.

Do not use phone number or raw account ID as the trace key. Keep identity in a controlled data system and pass a non-sensitive reference.

Propagate trace context through asynchronous boundaries, not only synchronous HTTP calls. Voice systems often cross WebSocket streams, message queues, provider webhooks, background tool workers, and SIP events. If a downstream service cannot carry the normal header, store an approved correlation token in its metadata and link the resulting span when the callback returns.

The call can outlive an individual process. A reconnect, transfer, or worker restart should create a new span under the same conversation trace rather than silently starting a second history. Record the relationship when the topology changes, especially during a warm transfer where caller, AI, and human media may briefly exist in separate rooms.

Test the trace itself with a synthetic call. Trigger a known tool delay, interruption, and handoff, then confirm that every event appears once, in a plausible order, with no sensitive value hiding in labels. Telemetry that is only inspected during an incident is usually incomplete when the incident arrives.

Spans for waits, events for moments

OpenTelemetry’s current event guidance draws a useful line:

Type Use it for Voice examples
Span An operation with a duration and a meaningful boundary speech.input, endpointing.wait, stt.finalize, model.response, tool.lookup_customer, tts.first_chunk, audio.playback
Event A state change or point-in-time occurrence with its own timestamp speech.partial_received, speech.final_received, caller.barge_in_detected, playback.stopped, entity.corrected, tool.side_effect_committed, handoff.requested

Keep names stable and put IDs or changing values in attributes rather than inside names. Use a versioned event dictionary, and define each timestamp from the component that can observe it. caller.speech_end should say whether it is the last received audio frame, the VAD estimate, or the endpointing decision. playback.started should mean audio reached your playback boundary, not merely that TTS returned bytes. Two teams can publish identically named events and still measure different intervals if that contract is missing.

Attach bounded attributes that explain the path: workflow, language lane, carrier route, codec, model family, tool name, result status, and whether the connection was warm. Avoid raw transcripts, phone numbers, account values, and unbounded prompt IDs. Put detailed evidence behind controlled references rather than turning the telemetry backend into another customer-data store.

The minimum timestamp set

Start with the minimum timestamp set for each caller turn:

Event Why it exists
caller.speech_start Detect overlap and user wait
caller.speech_end Anchor end-of-speech latency
endpoint.detected Isolate endpointing delay
stt.first_partial Observe early recognition
stt.final Observe finalization and partial churn
model.first_output Isolate model and orchestration wait
tool.start / tool.end Isolate external dependency time
tts.requested Measure orchestration gap before speech
tts.first_audio Measure synthesis startup
playback.started Isolate transport and buffering
barge_in.detected Anchor interruption response
playback.stopped Measure stop latency

Add more only when somebody will query it because telemetry nobody owns becomes storage. For each required event, name the producing service, an owner, the maximum expected missing rate, and the query that uses it. Missing telemetry should be visible. If 20 percent of calls lack playback.started, the end-of-speech metric is not “fast”; it is incomplete.

Instrument the business path manually

Automatic instrumentation will catch HTTP, database, and framework work, but it will not know that a transcript became final or that the caller corrected an amount. OpenTelemetry’s JavaScript guide shows how to initialize the SDK and create application spans. A trimmed voice-oriented example looks like this:

import { SpanStatusCode, trace } from "@opentelemetry/api";

const tracer = trace.getTracer("voice-turn", "1.0.0");

export async function runTurn(turnId: string, run: () => Promise<void>) {
  return tracer.startActiveSpan("voice.turn", async (span) => {
    span.setAttribute("voice.turn.id", turnId);

    try {
      await run();
      span.setStatus({ code: SpanStatusCode.OK });
    } catch (error) {
      span.recordException(error as Error);
      span.setStatus({ code: SpanStatusCode.ERROR });
      throw error;
    } finally {
      span.end();
    }
  });
}

Use your SDK’s named event or log-event API for point occurrences. OpenTelemetry’s event semantic conventions are still marked as development, so pin your packages and record the schema version. Create spans around operations you can improve rather than every helper function. The useful boundary is where a caller-visible wait begins and ends: endpointing, final transcription, model inference, tool access, normalization, synthesis, transport, and playback. Internal child spans can help the owning team, but the shared call trace should stay readable enough for an incident review.

Errors need status and outcome separately. A tool span can return without a network error and still produce a business rejection. A timeout can occur after the remote side committed the write. Record transport status, application status, and any durable side-effect ID so the latency investigation does not mistake an uncertain mutation for a harmless slow read.

Derive metrics after collection

Do not emit every dashboard number from application code. Derive caller and component metrics from the trace so their definitions can evolve without changing every service.

End-of-speech to first audible response is playback.started - caller.speech_end. This is the silence a caller experiences after finishing a turn, so it should be the headline latency measure for the lane rather than time to first model token.

Endpointing wait is endpoint.detected - caller.speech_end. It isolates the time spent deciding that the caller has stopped and reveals whether an apparently slow model is actually waiting on a conservative speech boundary.

Model path is model.first_output - stt.final. Keep tool work outside this span unless the model cannot produce a useful first chunk before the tool responds, in which case the dependency should remain visible as a child span.

TTS startup is tts.first_audio - tts.requested, while delivery overhead is playback.started - tts.first_audio. Separating them prevents a slow telephony buffer from being blamed on the voice provider and makes regional transport problems easier to spot.

Barge-in stop time is playback.stopped - barge_in.detected. Measure the moment audio actually stopped, not when the application sent a cancellation request, because buffered speech can continue after the request succeeds.

Partial churn counts how often a recognized entity changes before finalization. A fast first partial that rewrites the account number five times can trigger bad speculative work, even when the final transcript and average latency look fine.

Report p50, p90, p95, and the slow-call count by workflow because the p95 booking call and the p95 FAQ call may have different owners. Keep distributions rather than only stored percentiles. During an incident, you may need to separate a bimodal tool path, compare carriers, or inspect calls above a new threshold. Histograms preserve that flexibility without precomputing every possible slice.

Define a conversational budget for each lane. A simple FAQ might spend most of its time on endpointing and synthesis. A booking turn may reserve more time for an authoritative lookup. The overall budget stays tied to the caller experience, while the component allocations make trade-offs explicit. When one component improves, do not quietly let another consume the saved time.

Slice by the condition that causes the wait

Group traces by route and media conditions such as telephony route, edge region, and codec; by workload such as language, voice, workflow, tool path, model, and prompt version; and by call state such as warm or cold connection and whether an interruption occurred. Include audio condition only where it was lawfully captured and labeled. Use bounded categories and protected evidence links rather than high-cardinality labels that dump raw user values into telemetry.

Start every slow-call review from a caller-visible symptom. Was there silence after the caller stopped, a pause halfway through the agent response, late audio after an interruption, or a long wait during transfer? Each symptom maps to a different interval. This prevents a team from optimizing model first-token time while the caller is waiting on endpointing or phone delivery.

Compare slow traces with nearby normal traces from the same workflow and condition. The difference often reveals a cold connection, a specific tool branch, a particular codec, or a retry that a global percentile conceals. Keep deploy and configuration markers on the timeline so an operator can see whether the tail changed after a model, carrier, prompt, or tool release.

A fictional trace that points to the wrong team

Consider a fictional pharmacy refill agent. The call feels slow after the customer names a medicine, and the team blames the model because the first audible response arrives 1.7 seconds after the caller stops. The scenario and timings are invented, but they show how a real trace changes the diagnosis:

Component Duration
Endpointing 230 ms
STT finalization 140 ms
Model first output 190 ms
Formulary tool 930 ms
TTS startup 120 ms
Playback delivery 90 ms

The durations add up to 1.7 seconds, and the formulary tool owns most of the silence. The product team can speed up the lookup, cache safe data, issue an honest acknowledgement during the call while the lookup runs, or redesign the turn.

The acknowledgement itself needs evaluation. It should be truthful, short, and used only when the wait warrants it. Saying “I have updated that” before the tool finishes is not a latency technique; it is a false outcome claim. For longer operations, consider whether the workflow can gather another needed fact, offer a callback, or move to a human instead of filling silence.

Do not win the metric and lose the call

Time to first audio can be gamed with “Sure” followed by three seconds of nothing. Track first useful audio, tool-complete truth, and final outcome beside the timing trace because an agent that speaks before a write finishes is wrong with low latency. Voxeval joins the trace with conversation evaluation so latency gets credit only when the turn state and business outcome remain correct.

Use trace exemplars in release review. Select representative fast calls, tail calls, interrupted turns, tool failures, and route changes. Averages show whether the candidate moved; exemplars show why. If the new release improves first audio by speaking before a value is validated, the joined outcome check should make that regression visible.

Set alerts on changes that have an operator and a response. A p95 increase in tool.lookup_customer may page the service owner. A gradual rise in endpointing wait for one language lane may create an investigation ticket. An alert that says only “voice latency high” will bounce between teams until the caller stops complaining.

Use the same trace to diagnose streaming text-normalization delays and post-interruption recovery. Both can look like “the model was slow” until the spans separate the wait. The voice latency event schema lists starter spans, events, attributes, privacy notes, derived metrics, and likely owners to map to your stack.

Start with one complete trace, one slow call, and one owner. Once the team can explain that call end to end, scale the schema across the rest of the product. For more production voice AI instrumentation and evaluation guides, subscribe to Voxeval.

Reference list

Sources

  1. Your voice agent probably isn't slow because of the LLM
  2. OpenTelemetry JavaScript instrumentation
  3. OpenTelemetry semantic conventions for events
  4. How OpenAI delivers low-latency voice AI at scale