Header Logo

Building Robust Interrupt Handling for AI Avatars in Next.js and FastAPI Applications

Building Robust Interrupt Handling for AI Avatars in Next.js and FastAPI Applications

Robust interrupt handling for AI avatars in Next.js and FastAPI: idempotent barge-in, turn IDs, and safe cancellation of LLM, TTS, and video streams.

Introduction


Interrupt handling is one of the first places realtime avatar systems get brittle. A voice agent starts speaking, the user cuts in, the agent keeps generating audio, the video face lags behind, and now you have a bad conversational loop: the model is still talking about stale context while the UI looks unresponsive. In a Next.js + FastAPI stack, this usually shows up as a coordination problem across three layers: browser audio capture, server-side agent state, and the avatar/video stream.


This post focuses on the practical mechanics: how to detect interruption early, how to stop generation cleanly, how to keep the avatar, transcript, and session state consistent, and how to make the interrupt path idempotent so repeated user actions do not cause race conditions. By the end, you should be able to design a robust interrupt flow for a realtime avatar experience instead of treating “barge-in” as a UI afterthought.


What “interrupt” actually means in a realtime avatar system


In this context, an interrupt is not just a cancel button. It is a coordinated stop across multiple pipelines:


  • Audio input: the user starts speaking while the agent is speaking.

  • LLM generation: any in-flight response should stop producing tokens.

  • TTS / audio playback: currently buffered or streamed speech should be cut off.

  • Video/avatar state: lip-sync and facial motion should stop matching the aborted utterance.

  • Conversation state: the next response should be grounded in the new user turn, not the interrupted one.


If you do not explicitly model all five, you tend to fix one symptom and create another. For example, stopping the audio but leaving the model running can still append a useless assistant message to the transcript. Clearing the transcript without canceling the agent task can let the next response race with the stale one. Good interrupt handling is mostly about ordering and ownership.


Design the interrupt path as a state transition, not a button callback


The cleanest pattern is to treat the conversation as a small state machine with explicit transitions. At minimum, track:


  • listening — waiting for user speech

  • speaking — agent output is active

  • interrupting — a stop has been requested but cleanup is still in progress

  • idle — no active turn


In practice, the crucial rule is: an interrupt request should be idempotent. If the browser fires a barge-in event twice, or the user taps “stop” and then starts talking immediately, your backend should converge on one cancellation outcome.


Implement that with a turn identifier. Every agent response gets a unique turn_id. Interrupts target the current active turn only. If a late cancellation arrives for an already-finished turn, ignore it. That gives you a simple way to defeat races.


from dataclasses import dataclass

state: TurnState
from dataclasses import dataclass

state: TurnState
from dataclasses import dataclass

state: TurnState


When the user barge-ins, mark the active turn as interrupting, cancel downstream tasks, and only then clear or replace the active turn reference. That ordering matters because downstream components often need the current turn id to stop the correct work.


Browser-side barge-in: detect speech early, then signal the backend


In a Next.js frontend, the goal is to send an interrupt as soon as you have enough evidence that the user is taking the floor. Do not wait for a full utterance. Typical signals are:


  • Voice activity detection on the microphone stream

  • Pressed push-to-talk or explicit stop controls

  • Audio playback events, if you want to react to local user intent before VAD trips


The UI can optimistically mute or stop the avatar locally, but the backend still needs the authoritative interrupt so it can cancel generation. A practical flow looks like this:


  1. User starts speaking.

  2. Frontend detects speech onset.

  3. Frontend immediately disables avatar playback and sends an interrupt request.

  4. Backend cancels LLM/TTS work for the active turn.

  5. Backend acknowledges, and the frontend resumes with the new user turn.


Keep the client protocol simple. A short POST is enough if you already have a websocket or RTC session elsewhere:


fetch("/api/interrupt", {
});
fetch("/api/interrupt", {
});
fetch("/api/interrupt", {
});


Two gotchas show up often in Next.js apps:


  • Hydration and event timing: do not rely on component-local state alone. If the page re-renders while a turn is active, the active turn id should live in a store or server-backed session object.

  • Double submits: microphone VAD, keyboard shortcuts, and button clicks can all trigger the same interrupt. Debounce the UI, but still make the backend idempotent.


FastAPI backend: cancellation should reach every in-flight task


On the server, the interrupt endpoint should do three things: validate the session, locate the active turn, and cancel all work associated with that turn. In Python, that usually means keeping references to the active agent task, the streaming TTS task, and any websocket/RTC publisher responsible for the avatar stream.


from fastapi import FastAPI, HTTPException

return {"ok": True, "status": "cancelled"}
from fastapi import FastAPI, HTTPException

return {"ok": True, "status": "cancelled"}
from fastapi import FastAPI, HTTPException

return {"ok": True, "status": "cancelled"}


That example is intentionally small, but the same principles apply if your agent stack is more elaborate:


  • Use cooperative cancellation inside token streaming loops.

  • Stop TTS synthesis and flush any buffered audio frames.

  • Send a stream reset or end-of-utterance marker so the avatar renderer stops lip-syncing the aborted phrase.

  • Update conversation state only after cleanup succeeds, not before.


For realtime WebRTC-style systems, remember that cancellation does not rewind packets already in flight. You need both the control-plane cancel and the media-plane stop. If the transport is already sending frames, the receiver should treat the active turn as invalidated and discard late frames based on the turn id or equivalent session metadata.


Make the interrupt path race-safe


Most bugs here are timing bugs. A user interrupts exactly as the model finishes. The frontend sends an interrupt, but the backend has already moved to the next turn. Or the cancel lands while TTS is buffering and the avatar keeps moving for another half second. You can reduce this class of failure with three rules:


  1. Guard state transitions with the turn id. Every mutation should say which turn it belongs to.

  2. Separate “request to stop” from “cleanup finished”. The former changes state; the latter reclaims resources.

  3. Make stale messages harmless. Any late token, audio chunk, or transcript event should be ignored if its turn is no longer active.


In async Python, cancellation exceptions deserve special handling. Catch them only at the boundaries where you can safely release resources, then re-raise or finalize. Swallowing cancellation too early can leave long-running coroutines alive. On the browser side, prefer a single authoritative session store over scattered component state so a re-render does not resurrect an old turn.


Where Protoface fits


This is exactly the kind of coordination problem Protoface is meant to reduce. If you are using the LiveKit agent stack, the LiveKit plugin lets the avatar stay synchronized with the agent’s speaking state, so an interrupt can propagate through the same realtime session instead of being bolted on afterward. For Python integrations, the SDK and REST API are the places to manage sessions and issue control-plane actions with proper authentication; the exact request shapes are in the docs.


If you are already on Pipecat, there is also a dedicated integration guide in the Pipecat docs, and the quickstart repos linked from docs.protoface.com are useful for seeing the session lifecycle end-to-end. The important part is not the specific framework: it is that interruption is handled as a first-class session event, not as a UI hack.


Practical testing checklist


Before shipping, test the cases that usually fail in production:


  • User starts talking 100 ms before the agent finishes a sentence.

  • User clicks stop twice in a row.

  • User interrupts, then immediately starts a new question.

  • Network latency delays the interrupt acknowledgment.

  • The agent is mid-TTS chunk when cancellation arrives.


Instrument these paths with per-turn logs: turn id, state transitions, cancel timestamp, cleanup completion, and whether late frames were dropped. If you cannot explain a failed interrupt from logs alone, the system is not observably robust enough yet.


Conclusion


Robust interrupt handling is mostly a discipline problem: assign ownership to turns, propagate cancellation all the way through generation and playback, and make stale events harmless. Once you do that, the avatar feels responsive instead of stubborn, and your conversation state stays coherent even under barge-in.


If you want concrete implementation references, start with the docs and quickstarts at docs.protoface.com, then wire the interrupt path into your Next.js frontend and FastAPI control plane using the same turn identifiers end to end.

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.