> ## Documentation Index
> Fetch the complete documentation index at: https://lemonslice.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Production checklist for deploying LemonSlice avatars

> Production checklist for deploying LemonSlice avatars

<video className="hero-video" src="https://mintcdn.com/lemonslice/In5NRFQHKTq5DO3B/videos/hero.webm?fit=max&auto=format&n=In5NRFQHKTq5DO3B&q=85&s=50c8d52ef0dae51a2244d7f25be30d3a" controls data-path="videos/hero.webm" />

Clip from our [home page playground demo](https://lemonslice.com/) — a LiveKit agent built with the checklist on this page. We wrote these recommendations based on what we learned shipping it.

<hr />

## Use a ringing sound and UI

Avatar video takes a few seconds to initialize. A phone-call metaphor works well: users understand ringing, pickup, conversation, and hang-up.

<video className="hero-video" src="https://mintcdn.com/lemonslice/Pwz3AnAucmyyVpeh/videos/ringing-example.webm?fit=max&auto=format&n=Pwz3AnAucmyyVpeh&q=85&s=debbe3232528149cf9b166694f05d012" controls data-path="videos/ringing-example.webm" />

***

## Handle avatar connect/disconnect events

<Info>
  **Read this section if you experience any of the following issues:**

  * Empty room (user sees black screen)
  * User gets no video or audio, but audio/text input are still active
  * Avatar disappears
</Info>

Use the [`@lemonsliceai/avatar`](https://www.npmjs.com/package/@lemonsliceai/avatar) npm package on the frontend to detect when the avatar's first video frame has rendered, then switch from ringing to your **active call** UI. Listen for the avatar leaving to return to an **inactive** state so users can rejoin. See [Use a ringing sound and UI](#use-a-ringing-sound-and-ui) for the UX pattern.

<Tabs>
  <Tab title="LiveKit">
    Also listen for `ParticipantDisconnected` when the avatar leaves (call completion, idle timeout, errors, etc.).

    <Note>
      On the backend, listen for these events and call `agent_session.generate_reply()` when the LemonSlice avatar joins to prevent idle time before the avatar speaks. See the [LiveKit starter project](https://github.com/lemonsliceai/lemonslice-examples/tree/main/03-livekit-app-python) for a complete implementation.
    </Note>

    ```tsx theme={null}
    import { useState, useEffect } from "react";
    import { LiveKitRoom, RoomEvent, useRoomContext } from "@livekit/components-react";
    import { LiveKitAvatarReadyWatcher } from "@lemonsliceai/avatar/livekit-react";

    const AVATAR_IDENTITY = "lemonslice-avatar-agent";

    function AvatarCall({ serverUrl, token }: { serverUrl: string; token: string }) {
      const [avatarReady, setAvatarReady] = useState(false);

      return (
        <LiveKitRoom serverUrl={serverUrl} token={token} connect>
          <LiveKitAvatarReadyWatcher onReady={() => setAvatarReady(true)} />

          {!avatarReady && <RingingUI />}
          {avatarReady && <ActiveCallUI />}

          <DisconnectHandler />
        </LiveKitRoom>
      );
    }

    function DisconnectHandler() {
      const room = useRoomContext();

      useEffect(() => {
        const handleDisconnect = (participant) => {
          if (participant.identity === AVATAR_IDENTITY) {
            room.disconnect();
            // Swap to inactive call UI — user can rejoin from here
          }
        };

        room.on(RoomEvent.ParticipantDisconnected, handleDisconnect);
        return () => room.off(RoomEvent.ParticipantDisconnected, handleDisconnect);
      }, [room]);

      return null;
    }
    ```
  </Tab>

  <Tab title="Daily (Hosted)">
    LemonSlice events arrive on Daily's `app-message` channel. When the agent participant leaves, handle `participant-left` to reset your UI.

    See the [hosted Daily starter project](https://github.com/lemonsliceai/lemonslice-examples/tree/main/01-hosted-daily-app) for a full React implementation.

    ```tsx theme={null}
    import { useState, useRef, useEffect, useCallback } from "react";
    import { useDailyEvent, useMediaTrack } from "@daily-co/daily-react";
    import { useAvatarReady } from "@lemonsliceai/avatar/react";

    function AvatarCall() {
      const [avatarReady, setAvatarReady] = useState(false);
      const avatarTrack = useMediaTrack("LemonSlice Agent", "video");

      useAvatarReady(avatarTrack?.persistentTrack ?? null, {
        onReady: () => setAvatarReady(true),
      });

      useDailyEvent(
        "app-message",
        useCallback((ev) => {
          if (ev?.data?.type === "idle_timeout") {
            // Agent stopped due to inactivity — swap to inactive call UI
          }
        }, [])
      );

      useDailyEvent("participant-left", (ev) => {
        if (ev.participant?.user_name === "LemonSlice Agent") {
          // Swap to inactive call UI — user can start a new room
        }
      });

      return avatarReady ? <ActiveCallUI /> : <RingingUI />;
    }
    ```
  </Tab>
</Tabs>

***

## Handle room errors

<Info>
  **Read this section if you experience any of the following issues:**

  * Avatar fails to join a call
  * Crashed calls — avatar leaves or audio cuts out
</Info>

<Tabs>
  <Tab title="LiveKit">
    Listen for `Disconnected` events to handle network errors, WebRTC failures, or join failures.

    ```javascript theme={null}
    import { DisconnectReason, RoomEvent } from "livekit-client";

    room.on(RoomEvent.Disconnected, (reason) => {
      switch (reason) {
        case DisconnectReason.CLIENT_INITIATED:
          // User ended the call — swap to inactive call UI
          break;
        default:
          // Unexpected disconnect — swap to inactive call UI
      }
    });
    ```
  </Tab>

  <Tab title="Daily (Hosted)">
    Listen for `daily_error` app-messages and Daily's top-level error/left events.

    ```javascript theme={null}
    useDailyEvent("app-message", (ev) => {
      if (ev?.data?.type === "daily_error") {
        console.error(ev.data.error, ev.data.fatal);
        // Swap to inactive call UI if fatal
      }
    });

    useDailyEvent("error", (ev) => {
      console.error("Daily error:", ev);
      // Swap to inactive call UI
    });
    ```
  </Tab>
</Tabs>

***

## Catch pipeline errors

<Info>
  **Read this section if you experience any of the following issues:**

  * Avatar does not speak
  * Session dies unexpectedly
</Info>

<Tabs>
  <Tab title="LiveKit">
    Subscribe to `AgentSession` error events on your backend. Errors with `err.recoverable == False` mean the pipeline is dead — end the session gracefully.

    ```python theme={null}
    @session.on("error")
    def on_session_error(ev: ErrorEvent) -> None:
        err = ev.error
        if isinstance(err, TTSError):
            logger.error("AgentSession TTS error", exc_info=err.error)
        elif isinstance(err, STTError):
            logger.error("AgentSession STT error", exc_info=err.error)
        elif isinstance(err, LLMError):
            logger.error("AgentSession LLM error", exc_info=err.error)
        else:
            logger.error("AgentSession error", exc_info=err.error)
    ```
  </Tab>

  <Tab title="Daily (Hosted)">
    The Hosted Pipeline manages STT, LLM, and TTS for you. Monitor pipeline issues on the frontend via LemonSlice app-messages — especially `daily_error` and `video_generation_error`. See the [event reference](/hosted/integrations/daily-room-integration#receiving-events) for the full list.
  </Tab>
</Tabs>

***

## Handle startup failures

<Info>
  **Read this section if you experience any of the following issues:**

  * The call never connects
</Info>

If the avatar fails to join, give users a way to exit gracefully instead of waiting indefinitely.

<Tabs>
  <Tab title="LiveKit">
    The [LiveKit starter project](https://github.com/lemonsliceai/lemonslice-examples/tree/main/03-livekit-app-python) monitors agent startup and can send a failure message to the room. Catch it on the frontend:

    ```javascript theme={null}
    const ROOM_MESSAGE_TOPIC = "lemonslice/message";

    room.on(RoomEvent.DataReceived, (payload, participant, kind, topic) => {
      if (topic !== ROOM_MESSAGE_TOPIC) return;

      try {
        const message = JSON.parse(new TextDecoder().decode(payload));
        if (message.type === "startup_failure") {
          room.disconnect();
          // Swap to inactive call UI — user can retry
        }
      } catch (err) {
        console.error("Failed to parse data packet:", err);
      }
    });
    ```
  </Tab>

  <Tab title="Daily (Hosted)">
    Set a client-side timeout after creating the room. If `bot_ready` has not fired within your threshold (e.g. 30 seconds), call `leave()` and show a retry UI. Also handle `daily_error` with `fatal: true` during startup.
  </Tab>
</Tabs>

***

## Check timeouts

<Info>
  **Read this section if you experience any of the following issues:**

  * Avatar suddenly exits a call
  * Calls end after the same number of minutes every time
</Info>

Several timeouts can affect a session. Confirm each is set to the value you intend:

* **LemonSlice idle timeout** (default 60 seconds) — resets when the avatar is talking
* **LemonSlice GPU timeout** (default 30 minutes) — contact [support@lemonslice.com](mailto:support@lemonslice.com) if you need longer calls
* **Third-party timeouts** (LiveKit, Daily, ElevenLabs, etc.)

<Tabs>
  <Tab title="LiveKit">
    Set `idle_timeout` on `AvatarSession`:

    <Warning>
      Setting `idle_timeout` to `-1` disables the LemonSlice idle timeout. Ensure sessions are properly terminated to avoid stale calls and runaway billing.
    </Warning>

    <CodeGroup>
      ```python LiveKit (Python) theme={null}
      avatar = lemonslice.AvatarSession(
          agent_image_url="....",
          agent_prompt="a person talking.",
          idle_timeout=600,
      )
      session_id = await avatar.start(session, room=ctx.room)
      ```

      ```javascript LiveKit (Node.js) theme={null}
      const avatar = new lemonslice.AvatarSession({
        agentImageUrl: "...",
        agentPrompt: "a person talking.",
        idleTimeout: 600,
      });
      const sessionId = await avatar.start(session, room);
      ```

      ```python Pipecat (Python) theme={null}
      transport = LemonSliceTransport(
          ...
          session_request=LemonSliceNewSessionRequest(
              agent_image_url="...",
              agent_prompt="a person talking.",
              idle_timeout=600,
          ),
      )
      ```
    </CodeGroup>
  </Tab>

  <Tab title="Daily (Hosted)">
    Idle timeout is configured on your agent in the [LemonSlice web app](https://lemonslice.com/agents). When it fires, you'll receive an `idle_timeout` app-message — use it to reset your UI.

    End sessions explicitly with a `force-end` control event when the user hangs up:

    ```javascript theme={null}
    sendAppMessage({ event: "force-end" }, "*");
    ```
  </Tab>
</Tabs>

***

## Budget your latency

<Info>
  **Read this section if your avatar feels slow to respond** — including delayed replies after the user finishes speaking.
</Info>

If the conversation lags, the bottleneck is usually **your LLM** — not LemonSlice video. Check LLM response times and tool-calling latency first.

<Frame>
  <img src="https://mintcdn.com/lemonslice/wLzWQq1Uo3Mp5DG5/images/livekit-diagram-2.png?fit=max&auto=format&n=wLzWQq1Uo3Mp5DG5&q=85&s=aafc121b35714408c20d449f2c7b24fa" alt="Agent pipeline latency" width="2160" height="878" data-path="images/livekit-diagram-2.png" />
</Frame>

Humans expect a quick back-and-forth. Measure each pipeline step (STT, LLM, TTS, avatar video) and set a latency budget upfront. LLM times often creep from sub-second to three or four seconds as you add function calls, reasoning, or multimodal inputs. If a step blows your budget, move it off the critical path — run it async, set expectations with the user, or defer it.

## Use VAD to decrease perceived latency

Perceived latency matters more than absolute latency. Voice Activity Detection (VAD) lets you start reacting before a user has fully finished speaking, which effectively pulls your entire pipeline forward. Good turn detection means you can kick off generation as soon as intent is clear.

## Optional: Show a transcription

Displaying transcription can improve usability, especially for catching STT errors and making the agent's timing feel more predictable. Users can see when their speech has been "accepted," which reduces ambiguity around when the agent will respond.

However, transcription introduces its own UX risks. Many pipelines expose both fast, low-accuracy interim results and slower, higher-accuracy final transcripts (e.g., `interim_results=false` to suppress partials for Deepgram). When both are surfaced, users can see text rapidly change or correct itself, which feels unstable and undermines trust. We recommend disabling interim results if you choose to show transcriptions.
