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

# Stream Duration Limits

> How per-stream duration limits work, what the 'credit lease expired' error means, and how to keep streaming without interruption.

If you've seen an error like `Stream ended: credit lease expired` or `session_timeout` in your console, you're hitting the per-stream duration limit.
This is expected — each stream has a maximum lifetime, but your application can start a new one immediately to keep going.

## Two separate time limits

There are two clocks running independently during an interactive session:

| Limit              | Default     | What it covers                                  | What happens when it's reached                 |
| :----------------- | :---------- | :---------------------------------------------- | :--------------------------------------------- |
| **Per-stream**     | 150 seconds | A single `startStream()` → `endStream()` cycle  | Stream ends, but you can start another one     |
| **Per-connection** | 60 minutes  | The entire `connect()` → `disconnect()` session | Connection is closed, you'll need to reconnect |

The per-stream limit is the one most people hit first.
It applies to every stream individually, regardless of how much total usage your account allows.

<Note>
  Your account's total usage quota controls how many hours you can stream overall.
  The per-stream limit is separate — each individual stream maxes out at 150 seconds before you need to start the next one.
</Note>

## What the error looks like

When a stream hits its duration limit, the server sends a stream error with reason `session_timeout`.
In your browser console, you'll see something like:

```
[OdysseySDK] Full stream error:
{type: 'interactive_stream_error', reason: 'session_timeout',
 message: "Stream ended: credit lease expired"}
```

This fires through your `onStreamError` (JavaScript) or `on_stream_error` (Python) callback.
The stream is done, but the fix is straightforward: start a new one.

## Automatically restarting streams

The cleanest way to handle this is to catch the `session_timeout` error and reconnect.
After a lease expiry, the server closes the underlying connection, so you'll need to call `connect()` again before starting the next stream.

Here's the pattern for both SDKs:

<CodeGroup>
  ```typescript JavaScript SDK theme={null}
  import { Odyssey } from "@odysseyml/odyssey";

  const client = new Odyssey({ apiKey: "ody_your_api_key" });

  let currentPrompt = "A mountain lake at sunrise";

  async function startSession() {
    const mediaStream = await client.connect({
      onStreamError: async (reason, message) => {
        if (reason === "session_timeout") {
          // The stream expired and the server closed the connection.
          // Reconnect and pick up where we left off.
          console.log("Stream duration reached, restarting...");
          await client.connect();
          await client.startStream({ prompt: currentPrompt });
        } else {
          console.error(`Stream error: ${reason} — ${message}`);
        }
      },
      onStatusChange: (status, message) => {
        console.log(`Status: ${status}`, message ?? "");
      },
    });

    // Attach the media stream to a <video> element
    const video = document.getElementById("video") as HTMLVideoElement;
    video.srcObject = mediaStream;

    await client.startStream({ prompt: currentPrompt });
  }

  startSession();
  ```

  ```python Python SDK theme={null}
  import asyncio
  from odyssey import Odyssey

  client = Odyssey(api_key="ody_your_api_key")

  current_prompt = "A mountain lake at sunrise"
  reconnect_event = asyncio.Event()


  def handle_stream_error(reason: str, message: str) -> None:
      if reason == "session_timeout":
          # Stream expired — tell the main loop to reconnect.
          # Note: on_stream_error is called synchronously by the SDK,
          # so we signal an asyncio.Event and let the main loop handle it.
          print("Stream duration reached, restarting...")
          reconnect_event.set()
      else:
          print(f"Stream error: {reason} — {message}")


  async def run():
      while True:
          reconnect_event.clear()

          await client.connect(
              on_stream_error=handle_stream_error,
          )
          await client.start_stream(prompt=current_prompt)

          # Block until the stream expires
          await reconnect_event.wait()

          # Small pause so we don't hammer the server
          await asyncio.sleep(0.5)


  asyncio.run(run())
  ```
</CodeGroup>

There will be a brief visual gap (\~1-2 seconds) while the new connection spins up.
If you want to make this feel less jarring, show a loading state or freeze the last frame during the transition.

## React example

If you're building with React, you can wrap the reconnect logic in a hook:

```typescript theme={null}
import { useEffect, useRef, useCallback } from "react";
import { Odyssey } from "@odysseyml/odyssey";

function useOdysseyStream(apiKey: string, prompt: string) {
  const clientRef = useRef<Odyssey | null>(null);
  const videoRef = useRef<HTMLVideoElement>(null);

  const startStream = useCallback(async () => {
    const client = clientRef.current;
    if (!client) return;

    try {
      const mediaStream = await client.connect({
        onStreamError: async (reason) => {
          if (reason === "session_timeout") {
            await startStream();
          }
        },
      });

      if (videoRef.current) {
        videoRef.current.srcObject = mediaStream;
      }

      await client.startStream({ prompt });
    } catch (err) {
      console.error("Failed to start stream:", err);
    }
  }, [prompt]);

  useEffect(() => {
    clientRef.current = new Odyssey({ apiKey });
    startStream();

    return () => {
      clientRef.current?.disconnect();
      clientRef.current = null;
    };
  }, [apiKey, startStream]);

  return videoRef;
}
```

## Zero-downtime streaming with dual-session rollover

The reconnect patterns above work well, but there's a visible gap while the new connection spins up.
If you need truly seamless, uninterrupted video — for a live demo, a kiosk, or a production experience — you can eliminate the gap entirely by running two client instances and rolling over between them.

The idea is simple: you know each stream lasts 150 seconds, so around the 130-second mark, you start warming up a second connection in the background.
When it's ready, you swap the video source and let the first connection wind down.
The user never sees an interruption.

```typescript theme={null}
import { Odyssey } from "@odysseyml/odyssey";

const STREAM_DURATION = 150;    // seconds per stream
const WARMUP_LEAD_TIME = 20;    // start the next connection this many seconds before expiry

class DualSessionStream {
  private clients: [Odyssey, Odyssey];
  private activeIndex = 0;
  private videoElement: HTMLVideoElement;
  private prompt: string;
  private warmupTimer: ReturnType<typeof setTimeout> | null = null;

  constructor(apiKey: string, videoElement: HTMLVideoElement, prompt: string) {
    this.clients = [
      new Odyssey({ apiKey }),
      new Odyssey({ apiKey }),
    ];
    this.videoElement = videoElement;
    this.prompt = prompt;
  }

  async start() {
    await this.startOnClient(this.activeIndex);
  }

  stop() {
    if (this.warmupTimer) clearTimeout(this.warmupTimer);
    this.clients[0].disconnect();
    this.clients[1].disconnect();
  }

  private async startOnClient(index: number) {
    const client = this.clients[index];

    const mediaStream = await client.connect({
      onStreamError: async (reason) => {
        if (reason === "session_timeout") {
          // The standby client should already be live by now.
          // If not (e.g. warmup failed), fall back to a cold start.
          client.disconnect();
          if (this.videoElement.srcObject === mediaStream) {
            await this.startOnClient(index);
          }
        }
      },
    });

    // Attach video and start streaming
    this.videoElement.srcObject = mediaStream;
    await client.startStream({ prompt: this.prompt });
    this.activeIndex = index;

    // Schedule the rollover warmup
    this.scheduleWarmup(index);
  }

  private scheduleWarmup(currentIndex: number) {
    if (this.warmupTimer) clearTimeout(this.warmupTimer);

    const warmupDelay = (STREAM_DURATION - WARMUP_LEAD_TIME) * 1000;
    const nextIndex = currentIndex === 0 ? 1 : 0;

    this.warmupTimer = setTimeout(async () => {
      try {
        // Start the next client in the background
        await this.startOnClient(nextIndex);
        // Clean up the old one
        this.clients[currentIndex].disconnect();
      } catch (err) {
        console.error("Warmup failed, will cold-start on expiry:", err);
      }
    }, warmupDelay);
  }
}

// Usage
const video = document.getElementById("video") as HTMLVideoElement;
const session = new DualSessionStream("ody_your_api_key", video, "A mountain lake at sunrise");
session.start();
```

A few things to note about this pattern:

* **It uses two concurrent session slots.** Make sure your account's concurrent session limit can accommodate this.
* **The prompt stays the same across rollovers**, so the visual output picks up naturally. If your application changes prompts via `interact()`, make sure both clients stay in sync.
* **The 20-second lead time is generous** — connection setup usually takes 2-5 seconds, but the extra buffer accounts for cold starts and network variability. Tune this for your environment.

<Tip>
  This pattern works best for always-on experiences like kiosks, installations, or live event displays where any visual interruption is unacceptable.
  For most interactive applications, the simpler single-session reconnect pattern above is plenty.
</Tip>

## Things to keep in mind

* **Always wire up `onStreamError` / `on_stream_error`.** Without it, your video feed goes dead when the stream expires and the user has no idea what happened.
* **Hold onto the current prompt.** When you start the next stream, pass the same prompt so the visual picks up where it left off.
* **Add a short delay before reconnecting** (200–500ms is plenty) in the single-session pattern. This avoids slamming the server if something goes wrong and the error fires in a tight loop.
* **Show the user something during the gap** if you're using the single-session pattern. A brief spinner or "Restarting stream..." message goes a long way compared to a frozen or black frame.

## Related

<CardGroup cols={2}>
  <Card title="Connection vs. Stream" icon="arrows-split-up-and-left" href="/connection-vs-stream">
    How connections and streams relate to each other.
  </Card>

  <Card title="Session Management" icon="sliders" href="/session-management">
    Managing session lifecycle, cleanup, and concurrent limits.
  </Card>
</CardGroup>
