Header Logo

Debugging Latency in a Flask-Based Realtime Phone Agent with Avatar Streaming

Debugging Latency in a Flask-Based Realtime Phone Agent with Avatar Streaming

Debugging latency in Flask realtime phone agents: trace ASR, TTS, media, and avatar streaming to isolate bottlenecks and sync issues.

Introduction


Latency bugs in realtime phone agents are usually not one bug. They’re a chain: telephony ingress, ASR, agent inference, TTS, avatar synthesis, and streaming each add their own delay. When you add a Flask app in the middle, it’s easy to accidentally turn a “mostly realtime” system into one that feels sluggish and inconsistent.


This post is about debugging that chain systematically. By the end, you should be able to isolate where latency is coming from, measure it without fooling yourself, and decide whether the fix belongs in your Flask app, your media pipeline, or your avatar layer. I’ll also show where a realtime avatar layer fits when the agent needs a synchronized face rather than just audio.


First principle: measure the pipeline, not the symptom


With a realtime phone agent, the user’s experience is dominated by end-to-end turn latency:


  • caller speaks

  • audio is streamed into your app

  • speech is transcribed or routed to your agent

  • the model produces a response

  • audio is synthesized and returned

  • if you have an avatar, video frames are generated and streamed in sync


The mistake is to look only at “time to first audio byte” or “model response time” and assume that’s the whole story. In practice, you need timestamps at each boundary. At minimum, log:


  1. incoming media packet or utterance start

  2. ASR partial/final transcript time

  3. agent first-token or first-response time

  4. TTS start and first audio chunk time

  5. avatar session start and first visible frame time


Do not use wall-clock time from different machines unless clocks are synchronized. If you can’t use the same host clock, add an explicit request ID and propagate it through logs so you can reconstruct the path later.


Where Flask adds latency accidentally


Flask itself is not the problem; blocking code is. The common failure mode is treating a realtime system like a normal request/response web app. That leads to a few predictable issues:


  • Blocking I/O in the request handler: waiting on model calls, media uploads, or avatar setup before returning control.

  • Spawning work per request: creating clients, sessions, or subprocesses repeatedly instead of reusing them.

  • Running under a single worker: one long-lived streaming request can serialize unrelated work.

  • Chunking audio too aggressively: tiny frames increase overhead; huge frames increase buffering delay.

  • Logging synchronously: surprisingly common when every event gets written to disk or sent over the network inline.


If your Flask app is just the control plane, keep it that way. Use it to authenticate, create sessions, hand out ephemeral session state, and return quickly. Anything that needs continuous streaming should happen in the media or agent process, not inside a long-running Flask request.


Debug the media path separately from the agent path


Realtime phone systems have at least two independent latency budgets: media transport and agent inference. If you conflate them, you’ll chase the wrong fix.


Media path questions:


  • How long from caller audio to your server receiving a decoded frame?

  • Are you buffering entire utterances before passing them to the agent?

  • Are you transcoding on the hot path?

  • Are WebRTC jitter buffers, network loss, or packet retransmits introducing delay?


Agent path questions:


  • How long until the first partial transcript arrives?

  • How long until the model starts emitting a response?

  • How much time do you spend waiting on external tool calls?

  • Is your TTS queueing or cold-starting?


A useful debugging technique is to stub one side at a time. Feed prerecorded audio into the agent to remove telephony/network variability. Then swap in a trivial “echo” agent to isolate media and avatar streaming. When the latency disappears in one mode but not the other, the culprit is obvious.


# Example: instrument a Flask route that only creates a session and returns quickly

return jsonify(resp), 202
# Example: instrument a Flask route that only creates a session and returns quickly

return jsonify(resp), 202
# Example: instrument a Flask route that only creates a session and returns quickly

return jsonify(resp), 202


Avatar streaming makes latency visible, not just audible


When you add a realtime avatar, you’re no longer just debugging speech latency. You’re debugging synchronization. The face should start moving when the agent starts speaking, and mouth motion should stay aligned with audio. If the audio is snappy but the video starts half a second later, users will still perceive the system as laggy.


This is where people often make a subtle mistake: they treat video as a separate downstream task. For lip-synced avatars, it is not. The avatar stream should be attached to the same response lifecycle as the voice output, so frame generation, transport, and playout are coordinated.


Things to check when the face feels behind:


  • Are avatar frames being generated only after the full response is available?

  • Is the avatar process buffering too much audio before starting motion?

  • Are you sending the video stream over a separate path with a different jitter profile?

  • Does your UI wait for a “ready” event before rendering the video element?


In practice, you want incremental start behavior: as soon as the agent begins speaking, the avatar should begin streaming. Waiting for sentence completion before sending the first frame is a latency bug, not a polish issue.


Useful tracing shape for a Flask-based realtime agent


If you only add one thing, add structured event logging with correlation IDs. A simple event sequence can tell you more than an APM chart because it shows the handoff boundaries. For example:


{"request_id":"7b8...","event":"call_received","t_ms":0}
{"request_id":"7b8...","event":"avatar_first_frame","t_ms":667}
{"request_id":"7b8...","event":"call_received","t_ms":0}
{"request_id":"7b8...","event":"avatar_first_frame","t_ms":667}
{"request_id":"7b8...","event":"call_received","t_ms":0}
{"request_id":"7b8...","event":"avatar_first_frame","t_ms":667}


From there, the diagnosis is straightforward:


  • Large gap before asr_partial: media ingress or buffering issue.

  • Large gap between asr_partial and agent_first_token: model or tool latency.

  • Large gap between agent_first_token and tts_first_chunk: synthesis queue or text chunking problem.

  • Large gap before avatar_first_frame: avatar session startup or stream attachment issue.


That sequence is more actionable than “the call feels slow,” because it tells you which layer owns the fix.


One practical way to integrate Protoface


If your phone agent needs a visible face, keep the avatar attached to the realtime agent pipeline rather than trying to bolt it on afterward. The LiveKit plugin on PyPI is designed for that pattern, and the integration examples in the repo are a good starting point: GitHub. In a LiveKit-based agent, the plugin drops the avatar into the existing voice agent so audio and video stay coordinated instead of drifting apart.


# Illustrative only; consult the docs for exact fields and initialization details.

agent.add_avatar(avatar)
# Illustrative only; consult the docs for exact fields and initialization details.

agent.add_avatar(avatar)
# Illustrative only; consult the docs for exact fields and initialization details.

agent.add_avatar(avatar)


If you need to create or inspect sessions from your backend, use the REST API rather than doing that work inline in the request path. Keep Flask as the control plane, authenticate with API keys server-side, and let your media/agent worker own the realtime session lifecycle. The documentation has the precise request shapes and auth details: docs.protoface.com.


curl https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123","voice":"default"}'
curl https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123","voice":"default"}'
curl https://api.protoface.com/v1/sessions \
-d '{"avatar_id":"avt_123","voice":"default"}'


The main operational win is separation of concerns: Flask handles authorization and orchestration, the agent process handles streaming, and the avatar session stays synchronized with speech generation.


Common gotchas that look like “avatar latency” but aren’t


It’s worth calling out a few issues that often get blamed on the avatar layer even when the root cause is elsewhere:


  • Cold starts: the first turn after idle may include model, TTS, or websocket setup.

  • Overly large context: if every turn ships a giant prompt, the agent spends time serializing and reasoning before speaking.

  • Backpressure from UI rendering: a slow browser tab can delay perceived motion even if the stream is healthy.

  • Per-call initialization in Flask: recreating clients or fetching secrets on every request adds avoidable overhead.

  • Wrong concurrency model: a blocking WSGI worker can stall new calls while a current call is still active.


For development, start with one worker, one call, and deterministic test audio. Then add concurrency only after you can explain the timing of a single call. If you cannot predict where the 400 ms went, scaling the system will just make the problem harder to see.


Conclusion


When a Flask-based realtime phone agent feels slow, treat it like a distributed systems problem: measure each hop, keep control-plane work out of the streaming path, and verify whether the delay is in ingress, inference, synthesis, or avatar streaming. If you attach a synchronized avatar, make sure it starts with the audio rather than trailing it.


For implementation details, integration examples, and the exact API shapes, start with the docs and the relevant repo for your stack. The quickest path to a stable system is usually not “optimize everything,” but “make each boundary explicit, then remove the worst one.”

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.

Add a face to your AI.

No credit card needed.