Engineering

How to Reduce LiveKit Voice Agent Latency in 2026 With 12 Code-Level Fixes

Cut LiveKit voice-agent latency with current Agents 1.8 code: turn handling, preemptive generation, prompt caching, regional endpoints and per-turn latency metrics.

· Updated
· 15 min read
voice-ai 2026 livekit livekit-agents latency optimization how-to
How to reduce LiveKit agent latency: the STT, LLM and TTS pipeline of a voice agent turn with a latency stopwatch
Table of Contents

LiveKit Agents ships the orchestration. You ship the tuning. A caller asks a question and hears dead air before the agent answers, and that gap is your LiveKit agent latency.

Before you change anything, open the per-turn metrics on each ChatMessage, see whether end-of-turn delay, LLM time to first token or TTS time to first byte dominates, and apply the fix for that stage. This guide maps each of the 12 techniques from the parent voice latency methodology to the exact LiveKit Agents knob, plugin or callback, with Python code written against livekit-agents 1.8.8 (October 15, 2026).

TL;DR (which LiveKit setting fixes which latency stage)

#TechniqueLiveKit surfaceLatency stage it targets
0Measure per-turn latency firstChatMessage.metrics, per-plugin metrics_collectedEvery stage
1Streaming STT and preemptive generationStreaming STT; preemptive generation (on by default)End of turn to LLM start
2Partial LLM tokens into TTSBuilt in via AgentSessionTTS first audio
3Prompt prefix cachingStable system prompt, anthropic.LLM(caching="ephemeral")LLM time to first token
4Edge model routingself.update_options(llm=...) in on_user_turn_completedLLM time to first token
5Prefetch tool callsasyncio.create_task in on_user_turn_completedTool-call wait
6Instant connectwithPreConnectAudio / preConnectBuffer: true on the clientFirst turn
7Async evaluationtraceai-livekit spansScoring off the critical path
8Prewarmsetup_fnc, num_idle_processesFirst turn and cold starts
9Smaller models for short turnsself.update_options(llm=...)LLM time to first token
10Semantic cachesession.say then raise StopResponse()Skips LLM and TTS on hits
11KV-cache reuseStable chat_ctx order, provider cachingLater-turn LLM time to first token
12Regional endpointsAgent near its models; plugin base_urlNetwork round trips

Timeline of a LiveKit voice turn: end-of-turn delay, LLM time to first token and TTS first audio, each mapped to its fix

How to measure LiveKit agent latency before you tune

Measure first, so you fix the stage that is actually slow. The user and agent ChatMessage items of every turn carry a metrics report:

  • transcription_delay: time to get the transcript after the user stops speaking.
  • end_of_turn_delay: time between the end of speech and the decision to end the user’s turn.
  • llm_node_ttft: time for the LLM node to return its first token.
  • tts_node_ttfb: time for the TTS node to return its first chunk of audio.
  • e2e_latency: time from the user finishing speaking to the agent starting its response.

For a single STT, LLM, TTS or VAD call, subscribe to the per-plugin metrics_collected event, for example llm.on("metrics_collected", ...). The session-level metrics_collected event is deprecated; use session_usage_updated for usage. Log the numbers per turn, change one setting at a time, and compare.

traceai-livekit sends the same turns to Future AGI as spans, so each stage’s latency sits next to its eval scores (LiveKit integration docs). For the full span model, see how to trace voice agents with traceAI.

How to read this guide

The parent post covers each technique’s theory and the per-stage latency budget. This post is the LiveKit-specific implementation map. Each section below answers four questions:

  • What it does in LiveKit terms. Which plugin, which AgentSession parameter, which Agent callback.
  • Code. A 5-15 line Python snippet that runs against livekit-agents>=1.8.
  • Where LiveKit does it for you. If the framework handles the technique automatically.
  • Common mistake. What people break when they wire it the first time.

Pin SDK versions on every install. The examples here are written against livekit-agents==1.8.8, the October 15, 2026 release. Older versions ship older AgentSession signatures.

pip install "livekit-agents[anthropic,deepgram,openai,cartesia,silero,turn-detector]==1.8.8"
pip install traceai-livekit

1. Streaming STT and preemptive generation

What it does. Use streaming STT that emits interim transcripts while the user is still speaking, and let LiveKit start the LLM reply speculatively, before the end of turn is confirmed.

LiveKit surface. STT through LiveKit Inference or the Deepgram plugin streams by default. Preemptive generation is on by default in TurnHandlingOptions; if the chat context or tools change in on_user_turn_completed, the speculative reply is discarded and regenerated. LiveKit’s docs note that it increases LLM token usage and pays off less when users speak for long stretches (dictation, storytelling).

# livekit-agents==1.8.8
from livekit.agents import AgentSession, TurnHandlingOptions, inference
from livekit.plugins import silero

session = AgentSession(
    stt=inference.STT("deepgram/nova-3", language="multi"),
    llm=inference.LLM("google/gemma-4-31b-it"),
    tts=inference.TTS("cartesia/sonic-3"),
    vad=silero.VAD.load(),
    turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector()),  # preemptive generation is on by default
)

Where LiveKit does it for you. Preemptive generation is on by default, and you do not write the partial-transcript loop.

Common mistake. Assuming preemptive generation needs switching on, when it is on by default. The extra lever is preemptive_generation={"preemptive_tts": True} in TurnHandlingOptions, which also starts TTS before the turn is confirmed and cuts more latency at the cost of wasted compute on cancellations. LiveKit’s docs warn that preemptive generation doesn’t always reduce latency, so check the per-turn metrics after the change.

2. Partial LLM tokens piped into TTS

What it does. Stream LLM tokens. When the first sentence boundary lands, fire that sentence to TTS. The user hears the first word before the LLM has finished writing the response.

LiveKit surface. Fully automatic inside AgentSession. When the LLM plugin streams and the TTS plugin streams (Cartesia Sonic, ElevenLabs, OpenAI), the llm_node to tts_node pipe flushes at sentence boundaries with no extra config.

# livekit-agents==1.8.8
# Streaming LLM + streaming TTS is the default
session = AgentSession(
    stt=inference.STT("deepgram/nova-3"),
    llm=inference.LLM("google/gemma-4-31b-it"),  # streams by default
    tts=inference.TTS("cartesia/sonic-3"),    # streams by default
    vad=silero.VAD.load(),
)

Where LiveKit does it for you. Everything. The tts_node consumes the LLM token stream as an AsyncIterable[str] and flushes audio at sentence breaks.

Common mistake. Wrapping the LLM plugin in a custom llm_node that buffers the full response before returning. That kills the stream. If you need to override llm_node, return an AsyncIterable[llm.ChatChunk] and yield chunks as they arrive.

3. LLM prompt prefix caching

What it does. Anchor the system prompt at the top of the chat context. Keep it byte-identical across turns. Anthropic and OpenAI both cache reused prompt prefixes server-side, which cuts cost and latency on cache hits.

LiveKit surface. chat_ctx on the Agent class. The Anthropic plugin takes caching="ephemeral", which caches the system prompt, tools and chat history. OpenAI caches reused prompt prefixes above a minimum length (1,024 tokens for GPT-5.6 and later; it varies by request settings for earlier models). Build the prompt once, attach it to the Agent, and resist the urge to interpolate per-turn timestamps near the top.

# livekit-agents==1.8.8
from livekit.agents import Agent, llm
from livekit.plugins import anthropic

SYSTEM_PROMPT = """You are a customer support agent for Acme Corp.
You answer questions about orders, returns, and shipping.
You speak in short sentences."""  # keep the stable prefix above 512 tokens, the minimum cacheable length on Claude Haiku 5.5

class Assistant(Agent):
    def __init__(self) -> None:
        super().__init__(
            instructions=SYSTEM_PROMPT,
            llm=anthropic.LLM(
                model="claude-haiku-5-5",
                caching="ephemeral",  # system prompt, tools and chat history
            ),
        )

Prompts below the model’s minimum length are not cached, and no error is returned. Confirm hits through cache_read_input_tokens in the response usage.

Where LiveKit does it for you. chat_ctx preserves order across turns. The plugin marks the system prompt, tools and chat history for caching.

Common mistake. Putting the current timestamp or the user ID at the top of the system prompt for “personalization”. That changes the prefix byte-string every turn and defeats caching. Put dynamic content near the end of chat_ctx.

4. Edge model routing

What it does. Route short conversational turns to a smaller, faster model. Route complex tool turns to the larger model. Both LLMs hit the same chat_ctx so context is shared.

LiveKit surface. Override on_user_turn_completed on the Agent. Inspect the new message. Call self.update_options(llm=...) to swap the model on the live pipeline (the llm property is read-only).

# livekit-agents==1.8.8
from livekit.agents import Agent, llm
from livekit.plugins import openai, anthropic

LIGHT_LLM = openai.LLM(model="gpt-6-luna")
HEAVY_LLM = anthropic.LLM(model="claude-sonnet-5-5")

class Assistant(Agent):
    async def on_user_turn_completed(
        self, turn_ctx: llm.ChatContext, new_message: llm.ChatMessage
    ) -> None:
        text = new_message.text_content or ""
        if len(text) < 60 and "?" not in text:
            self.update_options(llm=LIGHT_LLM)
        else:
            self.update_options(llm=HEAVY_LLM)

Preemptive generation is on by default, and a reply it already started is reused when the transcript, chat context, tools and tool choice are unchanged after on_user_turn_completed; the model is not part of that check, so a swap made in this hook can apply from the next reply. Measure whether routing per turn or keeping preemption wins on your traffic.

Where LiveKit does it for you. Nothing. Routing is your call.

Common mistake. Routing every turn to the largest model “for quality”. Time to first token differs by model, so measure llm_node_ttft for both on your prompt. Short acknowledgments (“yes”, “thanks”, “can you repeat that”) do not need the larger model.

5. Prefetch tool calls on high-confidence intent

What it does. When the user’s turn completes, classify intent. If confidence is above 0.85 and the intent maps to a known @function_tool, fire the tool call in parallel with the LLM call. If the LLM picks a different tool, cancel the prefetched future.

LiveKit surface. on_user_turn_completed runs once per turn after the turn detector commits. Fire the tool as an asyncio task. Stash the future on the Agent instance. When the LLM later requests the tool inside the same turn, await the prefetched future instead of running it cold.

# livekit-agents==1.8.8
import asyncio
from livekit.agents import Agent, function_tool, llm, RunContext

class Assistant(Agent):
    def __init__(self) -> None:
        super().__init__(instructions="...")
        self._prefetch: asyncio.Task | None = None

    async def on_user_turn_completed(
        self, turn_ctx: llm.ChatContext, new_message: llm.ChatMessage
    ) -> None:
        text = (new_message.text_content or "").lower()
        if "order" in text and "status" in text:
            self._prefetch = asyncio.create_task(self._lookup_order(text))

    @function_tool
    async def get_order_status(self, ctx: RunContext, order_id: str) -> str:
        if self._prefetch is not None:
            try:
                return await self._prefetch
            finally:
                self._prefetch = None
        return await self._lookup_order(order_id)

Where LiveKit does it for you. Nothing. The decorator wires the function tool to the LLM. You wire the prefetch.

Common mistake. Forgetting to cancel the prefetched future when the LLM picks a different tool. Leaked tasks pile up at scale. Wrap the prefetch in a try/finally that cancels on exit if the future was not awaited.

6. Instant connect with pre-connect audio

What it does. Record the first audio the user speaks while the connection is still being set up, and hand it to the agent when it joins, so the first turn does not wait for the handshake.

LiveKit surface. The knob is on the client. Turn on instant connect (the pre-connect audio buffer) with withPreConnectAudio() (Swift, Flutter), withPreconnectAudio (Android) or preConnectBuffer: true (web JS). The SDK sends the buffered audio on the lk.agent.pre-connect-audio-buffer topic and discards it if no agent connects before the timeout.

Where LiveKit does it for you. The agent receives the buffered audio on the lk.agent.pre-connect-audio-buffer topic; you only enable it in the client SDK.

Common mistake. Skipping the client-side pre-connect option. Without it, nothing the user says before the connection is up reaches the agent.

7. Async evaluation

What it does. Run scoring after the turn commits. Never block the critical path on an LLM judge. Use a classifier model for inline rubrics if you absolutely need inline scoring.

LiveKit surface. traceai-livekit exports LiveKit’s native OpenTelemetry spans to Future AGI and maps them to Future AGI’s attributes. Span export is async. Scoring inside Future AGI’s Observe project runs off the critical path on the captured trace. Inline scoring only fires on the routes that need it.

# traceai-livekit, livekit-agents==1.8.8
from fi_instrumentation import register
from fi_instrumentation.fi_types import ProjectType
from traceai_livekit import enable_http_attribute_mapping

register(
    project_name="livekit-voice-agent",
    project_type=ProjectType.OBSERVE,
    set_global_tracer_provider=True,
)
enable_http_attribute_mapping()

Register inside the worker entrypoint, not in the module top level that the LiveKit job-runner pickles across processes. Otherwise you can hit pickling errors when LiveKit forks the agent worker.

Where Future AGI does it for you. Future AGI has custom evals, plus built-in checks such as Audio Quality, Conversation Coherence, Conversation Resolution and Task Completion, scored on captured traces off the critical path. You opt into inline scoring per route.

Common mistake. Running an LLM judge synchronously inside on_user_turn_completed “to gate the response”. That adds the judge’s full round trip to every turn. Score off the critical path with agent-learning-kit or on Future AGI, where BYOK LLM-as-judge evals run at $0 platform cost and heuristic evals are always free.

8. Prewarm, cold starts and what LiveKit already warms for you

What it does. Load model files and start provider connections before the caller speaks, so the first turn does not pay for setup.

LiveKit surface. The setup (prewarm) function runs before a job is assigned to the process, so load Silero VAD and other model files there. num_idle_processes sets how many warm processes wait: math.ceil(cpu_count) by default in production, 0 in development mode.

When the agent starts, and again when you swap a model, LiveKit calls prewarm() on the session’s STT, LLM and TTS; the Cartesia plugin uses it to warm its WebSocket pool. On LiveKit Cloud’s Build plan, agents get cold starts of 10 to 20 seconds once all sessions end (not on paid plans).

# livekit-agents==1.8.8
from livekit.agents import Agent, AgentServer, AgentSession, JobContext, JobProcess, TurnHandlingOptions, cli, inference
from livekit.plugins import silero

server = AgentServer()

def prewarm(proc: JobProcess) -> None:
    proc.userdata["vad"] = silero.VAD.load()

server.setup_fnc = prewarm

@server.rtc_session(agent_name="latency-demo")
async def entrypoint(ctx: JobContext) -> None:
    session = AgentSession(
        stt=inference.STT("deepgram/nova-3"),
        llm=inference.LLM("google/gemma-4-31b-it"),
        tts=inference.TTS("cartesia/sonic-3"),
        vad=ctx.proc.userdata["vad"],
        turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector()),
    )
    await session.start(room=ctx.room, agent=Agent(instructions="You are a helpful voice assistant."))

if __name__ == "__main__":
    cli.run_app(server)

Where LiveKit does it for you. Provider connection prewarm is automatic; you only move model loading into the setup function.

Common mistake. Loading Silero VAD or the turn detector inside entrypoint. Load model files in the setup function so they are ready before a job arrives; development mode keeps no warm processes (num_idle_processes defaults to 0).

9. Smaller models for short turns

What it does. Acknowledgments (“yes”, “okay”, “go on”) do not need a frontier model. Route them to a smaller LLM. Route tool turns and reasoning turns to the larger one.

LiveKit surface. Same hook as technique 4: on_user_turn_completed. Inspect the turn and call self.update_options(llm=...); the next reply uses the new model.

# livekit-agents==1.8.8
from livekit.agents import Agent, llm
from livekit.plugins import openai

NANO = openai.LLM(model="gpt-6-luna")
FULL = openai.LLM(model="gpt-6.1-sol")

class Assistant(Agent):
    async def on_user_turn_completed(
        self, turn_ctx: llm.ChatContext, new_message: llm.ChatMessage
    ) -> None:
        text = (new_message.text_content or "").strip()
        if text.split() and len(text.split()) <= 4:
            self.update_options(llm=NANO)
        else:
            self.update_options(llm=FULL)

Preemptive generation is on by default, and a reply it already started is reused when the transcript, chat context, tools and tool choice are unchanged after on_user_turn_completed; the model is not part of that check, so a swap made in this hook can apply from the next reply. Measure whether routing per turn or keeping preemption wins on your traffic.

Where LiveKit does it for you. Nothing. Routing is yours.

Common mistake. Routing based on token count rather than intent class. A four-word turn (“cancel my entire order”) may still need the heavier model. Use a tiny classifier or a hard list of acknowledgment phrases instead of raw length when stakes are high.

10. Semantic cache for common intents

What it does. Embed the user’s transcript. Search a cache of recently-answered queries by embedding similarity. On a hit above threshold, play the cached answer with session.say and skip the LLM call; pass cached audio frames through its audio argument to skip TTS as well.

LiveKit surface. on_user_turn_completed runs before the LLM. Check the cache there. On a hit, call session.say to play the cached answer, then raise StopResponse() so the agent skips its own reply.

# livekit-agents==1.8.8
from livekit.agents import Agent, StopResponse, llm

class Assistant(Agent):
    async def on_user_turn_completed(
        self, turn_ctx: llm.ChatContext, new_message: llm.ChatMessage
    ) -> None:
        text = new_message.text_content or ""
        hit = await self._semantic_cache.lookup(
            text,
            tenant_id=self._tenant_id,
            threshold=0.92,
        )
        if hit is not None:
            await self.session.say(hit.answer, allow_interruptions=False)
            raise StopResponse()

Where Future AGI does it for you. For teams that already route through the Agent Command Center, the gateway covers semantic caching, response caching, failover and routing across 100+ providers behind one endpoint.

Common mistake. Caching without a tenant ID. Customer A’s account-balance question must not return Customer B’s account balance. Always key the cache on tenant_id plus the intent embedding.

11. KV-cache reuse across turns

What it does. Multi-turn conversations on Anthropic and OpenAI can reuse cached prompt prefixes. The model skips reprocessing the conversation history that is already cached.

LiveKit surface. chat_ctx is preserved across turns by AgentSession. The plugin passes the same chat context to the LLM on every call. As long as the order is stable and the prefix has not changed, the provider hits the cache.

# livekit-agents==1.8.8
from livekit.agents import Agent
from livekit.plugins import anthropic

class Assistant(Agent):
    def __init__(self) -> None:
        super().__init__(
            instructions=SYSTEM_PROMPT,
            llm=anthropic.LLM(
                model="claude-sonnet-5-5",
                caching="ephemeral",
            ),
        )
    # AgentSession appends each turn to chat_ctx automatically.
    # Stable order = cache hits on turns 2+.

Where LiveKit does it for you. chat_ctx ordering is stable by default. You break it if you rewrite history mid-conversation.

Common mistake. Mutating chat_ctx in on_user_turn_completed to “tidy up” prior turns. Every mutation invalidates the cache from that point on. If you must rewrite history, do it at session start, not mid-conversation.

12. Host the agent next to its models and pin regional endpoints

What it does. Run the agent in the same region as its STT, LLM and TTS endpoints, so each model call avoids a long network round trip.

LiveKit surface. LiveKit Inference routes to providers without regional restriction by default (new EU-hosted projects have region restriction on by default), and you can turn region restriction on. Plugins call the provider directly, so set its regional endpoint: the Deepgram plugin takes base_url (default https://api.deepgram.com/v1/listen, EU https://api.eu.deepgram.com/v1/listen), and the Cartesia plugin also takes base_url.

# livekit-agents==1.8.8
from livekit.plugins import deepgram, cartesia

stt = deepgram.STT(
    model="nova-3",
    interim_results=True,
    base_url="https://api.eu.deepgram.com/v1/listen",  # EU endpoint
)

tts = cartesia.TTS(
    model="sonic-3",
    voice="9626c31c-bec5-4cca-baa8-f8ba9e84c8bc",
)

Where LiveKit does it for you. LiveKit routes media over its own network. Provider-side region is on the plugin.

Common mistake. Hosting the agent far from its STT, LLM and TTS. LiveKit’s latency playbook lists agent-model co-location among its top fixes; with LiveKit Inference turn on region restriction, and with plugins set each provider’s regional endpoint.

How turn detection and endpointing affect LiveKit latency

LiveKit’s audio turn detector, inference.TurnDetector(), reads the user’s audio directly, both the words and cues like intonation and rhythm, to decide when the user has finished speaking. LiveKit’s tuning docs use it in their recommended config; the text-based MultilingualModel and EnglishModel are deprecated. The latency levers are on TurnHandlingOptions. For the background, see how voice agents detect when a caller stops talking.

# livekit-agents==1.8.8
from livekit.agents import AgentSession, TurnHandlingOptions, inference
from livekit.plugins import silero

session = AgentSession(
    stt=inference.STT("deepgram/nova-3", language="multi"),
    llm=inference.LLM("google/gemma-4-31b-it"),
    tts=inference.TTS("cartesia/sonic-3"),
    vad=silero.VAD.load(
        min_silence_duration=0.4,
        prefix_padding_duration=0.2,
    ),
    turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector(), endpointing={"mode": "dynamic", "min_delay": 0.3, "max_delay": 2.0}),
)

Endpointing min_delay is the floor and max_delay the ceiling; the defaults are fixed mode with 0.5 s and 3.0 s, or 0.3 s and 2.5 s with the audio turn detector. Lower min_delay to respond sooner at the cost of more false turn boundaries on noisy audio. In Python, "mode": "dynamic" adapts the delay within that range from the session’s pause statistics.

The Silero VAD min_silence_duration (default 0.55 s) sets how long a silence must last before the VAD flips to “not speaking”; lower values end turns sooner, but the audio turn detector needs at least 0.25 s. Noise cancellation (voice isolation or background noise suppression) runs before VAD and STT, so it cleans every turn signal downstream; for SIP calls use the telephony model, BVCTelephony.

For push-to-talk workloads, set turn_handling=TurnHandlingOptions(turn_detection="manual") and drive turns with session.commit_user_turn and session.clear_user_turn. The user controls the boundaries, so there is no endpointing delay to wait for.

A LiveKit agent with every fix applied

The full reference agent combines streaming STT, partial TTS, edge routing, prefetch, async eval, prewarm and smaller models for short turns. It registers traceAI inside the entrypoint, loads Silero VAD in the setup function and hands turn detection to inference.TurnDetector().

Future AGI traceAI trace of a stacked LiveKit pipeline showing per stage STT, LLM and TTS spans with time to first token and latency attributes on a dark background

# livekit-agents==1.8.8, traceai-livekit
import asyncio
from livekit.agents import (
    Agent, AgentServer, AgentSession, JobContext, JobProcess, TurnHandlingOptions, cli,
    inference, llm, function_tool, RunContext,
)
from livekit.plugins import silero, deepgram, cartesia, openai
from fi_instrumentation import register
from fi_instrumentation.fi_types import ProjectType
from traceai_livekit import enable_http_attribute_mapping

SYSTEM_PROMPT = "You are an Acme support agent. Speak in short sentences."
LIGHT_LLM = openai.LLM(model="gpt-6-luna")
HEAVY_LLM = openai.LLM(model="gpt-6.1-sol")

class Assistant(Agent):
    def __init__(self) -> None:
        super().__init__(instructions=SYSTEM_PROMPT, llm=LIGHT_LLM)
        self._prefetch: asyncio.Task | None = None

    async def on_user_turn_completed(
        self, turn_ctx: llm.ChatContext, new_message: llm.ChatMessage
    ) -> None:
        text = (new_message.text_content or "").lower()
        self.update_options(llm=LIGHT_LLM if len(text.split()) <= 6 else HEAVY_LLM)
        if "order" in text and "status" in text:
            self._prefetch = asyncio.create_task(self._lookup_order(text))

    @function_tool
    async def get_order_status(self, ctx: RunContext, order_id: str) -> str:
        if self._prefetch is not None:
            try:
                return await self._prefetch
            finally:
                self._prefetch = None
        return await self._lookup_order(order_id)

    async def _lookup_order(self, text: str) -> str:
        # your real lookup
        return "Order shipped on Monday."

def prewarm(proc: JobProcess) -> None:
    proc.userdata["vad"] = silero.VAD.load(min_silence_duration=0.4)

server = AgentServer()

server.setup_fnc = prewarm

@server.rtc_session(agent_name="acme-support")
async def entrypoint(ctx: JobContext) -> None:
    register(
        project_name="acme-livekit-voice",
        project_type=ProjectType.OBSERVE,
        set_global_tracer_provider=True,
    )
    enable_http_attribute_mapping()

    session = AgentSession(
        stt=deepgram.STT(model="nova-3", interim_results=True),
        llm=LIGHT_LLM,
        tts=cartesia.TTS(model="sonic-3"),
        vad=ctx.proc.userdata["vad"],
        turn_handling=TurnHandlingOptions(turn_detection=inference.TurnDetector()),
    )
    await session.start(room=ctx.room, agent=Assistant())

if __name__ == "__main__":
    cli.run_app(server)

Run it, then read end_of_turn_delay, llm_node_ttft, tts_node_ttfb and e2e_latency on each turn’s ChatMessage.metrics to see which stage still dominates. Change one setting at a time and compare the same fields before and after.

How to keep LiveKit latency low after launch

LiveKit gives you per-turn metrics and OTel export; Future AGI turns those traces into scored, clustered, guarded production data. LiveKit agents connect through traceAI: install traceai-livekit, register inside the entrypoint, and each turn lands in Observe with STT, LLM and TTS spans. The package is part of the traceAI family alongside a dedicated Pipecat integration.

# traceai-livekit, run inside the worker entrypoint
from fi_instrumentation import register
from fi_instrumentation.fi_types import ProjectType
from traceai_livekit import enable_http_attribute_mapping

register(
    project_name="livekit-voice-agent",
    project_type=ProjectType.OBSERVE,
    set_global_tracer_provider=True,
)
enable_http_attribute_mapping()

Register in-process inside the LiveKit worker entrypoint, not at module top level. The LiveKit job-runner forks workers and you can hit pickling issues if the tracer provider is captured before fork.

Each turn lands in Observe with span-level depth: speech-to-text, the model call, tool calls and text-to-speech. Error Feed clusters errors from those traces so you can investigate them in one place.

For inline guardrails on the LLM response, Future AGI Protect runs named checks such as PII detection and prompt injection on text, image and audio inputs, and returns block, warn, mask or log. The Protect paper (arXiv 2510.13351) trains LoRA adapters on Gemma 3n (gemma-3n-E4B-it) for four safety dimensions (toxicity, sexism, data privacy and prompt injection) and reports a median time-to-label of 65 ms on text (p95 74 ms) in its Explanation Assistant serving setup.

To score the audio itself, Future AGI has custom evals plus built-in checks such as Audio Quality and Conversation Resolution; the built-in evals list shows what each one scores. Six prompt-optimizer algorithms (Random Search, Bayesian Search, ProTeGi, Meta-Prompt, PromptWizard, GEPA per arXiv 2507.19457) ship Apache-2.0 in agent-learning-kit.

Once your LiveKit traces accumulate failure patterns in Future AGI, point an optimization run at the dataset, pick an evaluator, pick one of the six optimizers, and review candidate prompts in the dashboard. Future AGI Cloud carries the certifications listed on futureagi.com/trust (SOC 2 Type II, HIPAA, GDPR, CCPA, ISO 27001), or you can self-host the Apache-2.0 core with Docker Compose or Helm.

Back to that caller and the dead air. Fixing one slow turn is a start; keeping turns fast after every prompt, model or plugin change needs the same measurement on every call. Trace each turn with traceai-livekit, score the spans with evals, set alerts for latency regressions, and simulate voice calls before each release: Future AGI Simulation runs voice or chat scenarios with personas and reports latency, interruptions and talk ratio.

Trace your next LiveKit call and find the stage that adds the dead air with Future AGI Observe.

Frequently Asked Questions

How to reduce latency in LiveKit?

To reduce latency in LiveKit, host the agent close to its STT, LLM and TTS providers, then tune turn handling and measure every change. Keep preemptive generation on, try preemptive_tts, lower endpointing min_delay or switch to dynamic mode, and keep idle processes warm. Check per-turn metrics after each change.

What LiveKit latency metrics should I track?

Track the per-turn LiveKit latency metrics on ChatMessage.metrics: transcription_delay, end_of_turn_delay, llm_node_ttft, tts_node_ttfb and e2e_latency, which together show where each turn loses time. Per-plugin metrics_collected events break out single STT, LLM and TTS calls. With traceai-livekit, those turns land in Future AGI Observe as spans you can score with evals.

What causes LiveKit latency issues?

LiveKit latency issues come mainly from the agent pipeline, not the WebRTC transport: endpointing delay, LLM time to first token, TTS time to first byte, and distant providers. Cold starts on the Build plan and zero idle processes in development mode also slow the first turn.

Is LiveKit low latency?

Yes, LiveKit is built for low latency: its WebRTC transport is optimized for real-time audio, and LiveKit says the client-to-server leg is a small share of agent latency. For phone calls, keep the SIP trunk and the agent in the same region, because a transatlantic hop adds noticeable delay.

Does preemptive generation reduce LiveKit agent latency?

Yes, preemptive generation can reduce LiveKit agent latency, because it starts the LLM response speculatively, before the end of the user's turn is confirmed. It is on by default, but LiveKit's docs warn it doesn't always reduce latency, so compare per-turn metrics. Turning on preemptive_tts cuts more, at the cost of wasted compute.

How do I tune endpointing delay in LiveKit?

Tune endpointing delay in LiveKit through TurnHandlingOptions, where min_delay and max_delay default to 0.5 and 3.0 seconds, or 0.3 and 2.5 seconds with the audio turn detector. Lower min_delay to cut dead air, or switch Python agents to dynamic mode. The old min_endpointing_delay argument is deprecated, and a very low delay can cut users off mid-pause.

Should I use LiveKit Inference or provider plugins for lower latency?

Use LiveKit Inference when you want LiveKit to manage model access, and provider plugins when you need a specific provider's regional endpoint. Latency depends on keeping models near the agent. Inference routes without regional restriction by default, except new EU-hosted projects where region restriction is on by default, and LiveKit recommends Gemma 4 31B as its latency-optimized default LLM.
Related Articles
View all