Frontier Engineering
← Voice & Realtime AI

Latency Budget to First Audio

Also known as: time to first byte of speech, perceived latency, TTFA

Treat time-to-first-audible-syllable as the product metric, and engineer every stage against an explicit allocation.

9 min read

The problem

Voice teams optimize total response time, which is the wrong number. What a caller experiences is the silence between when they stop speaking and when they hear something — after that, generation speed barely matters as long as it keeps up with playback. Optimizing the wrong metric leads to work on the LLM's throughput while a 400ms endpointing threshold and a non-streaming TTS call go unexamined.

The pattern

Define time-to-first-audio as the primary SLO, allocate an explicit millisecond budget to every stage in the chain, and instrument each stage separately so a regression is attributable. Then apply the standard levers in the order they pay: stream everything, overlap stages speculatively, warm connections, and — where a stage genuinely cannot be made fast — cover it with speech rather than silence.

Use when

  • Any real-time voice product, especially telephony where callers expect phone-call timing
  • Perceived responsiveness is a differentiator or a source of complaints
  • The pipeline has more than two stages and regressions are hard to attribute
  • Some turns require slow tool calls that blow through the budget

Avoid when

  • Asynchronous or recorded voice, where nobody is waiting in real time
  • Push-to-talk interfaces where users already expect a processing pause
  • Very early prototypes — get the conversation working before you optimize its timing

Tradeoffs

  • GainA clear metric everyone can optimize against, with per-stage attribution

    CostInstrumentation at every boundary, and an SLO someone has to own

  • GainSpeculative execution can remove an entire stage from the critical path

    CostWasted compute on speculations that are discarded when the user keeps talking

  • GainFiller speech makes an unavoidable 3-second tool call tolerable

    CostFillers used reflexively become a verbal tic users notice and dislike


The number to hold in mind: in natural conversation, the gap between turns averages around 200ms, and people begin reading a gap as hesitation somewhere past 500ms. That is the bar. A 2-second pause is not slightly slow, it is a different kind of interaction.

Endpointing is usually the biggest line — and the one teams optimise last.

Allocate, then instrument

Write the budget down as an object in code, emit a timing event at every boundary, and alert on the stages rather than the total. A p95 regression in TTS connection time looks identical to one in LLM time-to-first-token if all you track is the sum.

BUDGET_MS = {
    "endpoint":   350,   # adaptive silence detection
    "stt_final":  100,   # finalize the partial transcript
    "llm_ttft":   350,   # first token out of the model
    "tts_ttfa":   120,   # first audio chunk out of TTS
    "transport":  100,   # network + jitter buffer
}                        # total target: 1020 ms


class TurnTimer:
    def __init__(self, turn_id: str):
        self.turn_id, self.t0, self.marks = turn_id, time.perf_counter(), {}

    def mark(self, stage: str) -> None:
        elapsed = (time.perf_counter() - self.t0) * 1000
        self.marks[stage] = elapsed - sum(self.marks.values())
        metrics.histogram(f"voice.stage.{stage}", self.marks[stage])
        if self.marks[stage] > BUDGET_MS[stage] * 1.5:
            log.warning("stage over budget", extra={"stage": stage, "ms": self.marks[stage]})

    def first_audio(self) -> None:
        metrics.histogram("voice.ttfa", (time.perf_counter() - self.t0) * 1000)

Speculative generation

The endpointing wait is dead time — the user has stopped, and you are only waiting to be sure. Use it: when speech pauses and the partial transcript looks complete, start generating against it immediately. If the user resumes, cancel and discard. If they do not, the model has a head start equal to the entire endpointing threshold, which is typically the largest single line in the budget.

async def on_speech_pause(self, partial: str) -> None:
    if not looks_complete(partial):
        return
    self.speculation = asyncio.create_task(self.generate(partial))


async def on_speech_resumed(self) -> None:
    if self.speculation:
        self.speculation.cancel()   # user kept talking; throw the work away
        self.speculation = None
        metrics.increment("voice.speculation.discarded")


async def on_endpoint(self, final: str) -> AsyncIterator[bytes]:
    if self.speculation and normalized(final) == normalized(self.speculative_input):
        metrics.increment("voice.speculation.hit")
        return await self.speculation      # entire endpoint window saved
    if self.speculation:
        self.speculation.cancel()
    return await self.generate(final)
tip

Track speculation hit rate against wasted token spend. Above roughly 60% hits it is clearly worth it; below 30% you are burning money for little latency gain, and the endpointing heuristic is what needs fixing.

Covering unavoidable slowness

Some turns cannot be fast: a database lookup takes two seconds, and no pipeline work changes that. Cover it with speech rather than silence — but do so conditionally, only when the wait actually exceeds a threshold, and vary the phrasing. A filler on every turn becomes a tic; a filler on the 5% of turns that are genuinely slow reads as natural.

FILLERS = ["Let me check that.", "One moment.", "Looking that up now."]


async def with_filler(coro, threshold_s: float = 0.9):
    task = asyncio.create_task(coro)
    done, _ = await asyncio.wait({task}, timeout=threshold_s)
    if not done:
        await speak(random.choice(FILLERS))   # only when it is really slow
    return await task
  • Time to first audio is the SLO; total response time is not the user's experience
  • Instrument every boundary and alert per stage, never on the sum alone
  • Speculate during the endpoint window — it is usually the biggest line in the budget
  • Warm TTS and LLM connections at call start; cold TLS handshakes cost 100–200ms
  • Use fillers conditionally on real slowness, and rotate the phrasing
warning

Measure on the real transport. A pipeline that hits 600ms on localhost can land at 1.4 seconds over a PSTN leg with jitter buffering and codec transcoding. Benchmark end to end from an actual phone, not from your laptop.