📄 ai-sdk/docs/ai-sdk-ui/chatbot-resume-streams

File: chatbot-resume-streams.md | Updated: 11/15/2025

Source: https://ai-sdk.dev/docs/ai-sdk-ui/chatbot-resume-streams

AI SDK

Menu

v5 (Latest)

AI SDK 5.x

AI SDK by Vercel

AI SDK 6 Beta

Foundations

Overview

Providers and Models

Prompts

Tools

Streaming

Getting Started

Navigating the Library

Next.js App Router

Next.js Pages Router

Svelte

Vue.js (Nuxt)

Node.js

Expo

Agents

Agents

Building Agents

Workflow Patterns

Loop Control

AI SDK Core

Overview

Generating Text

Generating Structured Data

Tool Calling

Model Context Protocol (MCP) Tools

Prompt Engineering

Settings

Embeddings

Image Generation

Transcription

Speech

Language Model Middleware

Provider & Model Management

Error Handling

Testing

Telemetry

AI SDK UI

Overview

Chatbot

Chatbot Message Persistence

Chatbot Resume Streams

Chatbot Tool Usage

Generative User Interfaces

Completion

Object Generation

Streaming Custom Data

Error Handling

Transport

Reading UIMessage Streams

Message Metadata

Stream Protocols

AI SDK RSC

Advanced

Reference

AI SDK Core

AI SDK UI

AI SDK RSC

Stream Helpers

AI SDK Errors

Migration Guides

Troubleshooting

Copy markdown

Chatbot Resume Streams

==========================================================================================================

useChat supports resuming ongoing streams after page reloads. Use this feature to build applications with long-running generations.

Stream resumption is not compatible with abort functionality. Closing a tab or refreshing the page triggers an abort signal that will break the resumption mechanism. Do not use resume: true if you need abort functionality in your application. See troubleshooting for more details.

How stream resumption works


Stream resumption requires persistence for messages and active streams in your application. The AI SDK provides tools to connect to storage, but you need to set up the storage yourself.

The AI SDK provides:

  • A resume option in useChat that automatically reconnects to active streams
  • Access to the outgoing stream through the consumeSseStream callback
  • Automatic HTTP requests to your resume endpoints

You build:

  • Storage to track which stream belongs to each chat
  • Redis to store the UIMessage stream
  • Two API endpoints: POST to create streams, GET to resume them
  • Integration with resumable-stream to manage Redis storage

Prerequisites


To implement resumable streams in your chat application, you need:

  1. The resumable-stream package - Handles the publisher/subscriber mechanism for streams
  2. A Redis instance - Stores stream data (e.g. Redis through Vercel )
  3. A persistence layer - Tracks which stream ID is active for each chat (e.g. database)

Implementation


1. Client-side: Enable stream resumption

Use the resume option in the useChat hook to enable stream resumption. When resume is true, the hook automatically attempts to reconnect to any active stream for the chat on mount:

app/chat/[chatId]/chat.tsx

'use client';
import { useChat } from '@ai-sdk/react';import { DefaultChatTransport, type UIMessage } from 'ai';
export function Chat({  chatData,  resume = false,}: {  chatData: { id: string; messages: UIMessage[] };  resume?: boolean;}) {  const { messages, sendMessage, status } = useChat({    id: chatData.id,    messages: chatData.messages,    resume, // Enable automatic stream resumption    transport: new DefaultChatTransport({      // You must send the id of the chat      prepareSendMessagesRequest: ({ id, messages }) => {        return {          body: {            id,            message: messages[messages.length - 1],          },        };      },    }),  });
  return <div>{/* Your chat UI */}</div>;}

You must send the chat ID with each request (see prepareSendMessagesRequest).

When you enable resume, the useChat hook makes a GET request to /api/chat/[id]/stream on mount to check for and resume any active streams.

Let's start by creating the POST handler to create the resumable stream.

2. Create the POST handler

The POST handler creates resumable streams using the consumeSseStream callback:

app/api/chat/route.ts

import { openai } from '@ai-sdk/openai';import { readChat, saveChat } from '@util/chat-store';import {  convertToModelMessages,  generateId,  streamText,  type UIMessage,} from 'ai';import { after } from 'next/server';import { createResumableStreamContext } from 'resumable-stream';
export async function POST(req: Request) {  const {    message,    id,  }: {    message: UIMessage | undefined;    id: string;  } = await req.json();
  const chat = await readChat(id);  let messages = chat.messages;
  messages = [...messages, message!];
  // Clear any previous active stream and save the user message  saveChat({ id, messages, activeStreamId: null });
  const result = streamText({    model: openai('gpt-4o-mini'),    messages: convertToModelMessages(messages),  });
  return result.toUIMessageStreamResponse({    originalMessages: messages,    generateMessageId: generateId,    onFinish: ({ messages }) => {      // Clear the active stream when finished      saveChat({ id, messages, activeStreamId: null });    },    async consumeSseStream({ stream }) {      const streamId = generateId();
      // Create a resumable stream from the SSE stream      const streamContext = createResumableStreamContext({ waitUntil: after });      await streamContext.createNewResumableStream(streamId, () => stream);
      // Update the chat with the active stream ID      saveChat({ id, activeStreamId: streamId });    },  });}

3. Implement the GET handler

Create a GET handler at /api/chat/[id]/stream that:

  1. Reads the chat ID from the route params
  2. Loads the chat data to check for an active stream
  3. Returns 204 (No Content) if no stream is active
  4. Resumes the existing stream if one is found

app/api/chat/[id]/stream/route.ts

import { readChat } from '@util/chat-store';import { UI_MESSAGE_STREAM_HEADERS } from 'ai';import { after } from 'next/server';import { createResumableStreamContext } from 'resumable-stream';
export async function GET(  _: Request,  { params }: { params: Promise<{ id: string }> },) {  const { id } = await params;
  const chat = await readChat(id);
  if (chat.activeStreamId == null) {    // no content response when there is no active stream    return new Response(null, { status: 204 });  }
  const streamContext = createResumableStreamContext({    waitUntil: after,  });
  return new Response(    await streamContext.resumeExistingStream(chat.activeStreamId),    { headers: UI_MESSAGE_STREAM_HEADERS },  );}

The after function from Next.js allows work to continue after the response has been sent. This ensures that the resumable stream persists in Redis even after the initial response is returned to the client, enabling reconnection later.

How it works


Request lifecycle

Diagram showing the architecture and lifecycle of resumable stream requests

The diagram above shows the complete lifecycle of a resumable stream:

  1. Stream creation: When you send a new message, the POST handler uses streamText to generate the response. The consumeSseStream callback creates a resumable stream with a unique ID and stores it in Redis through the resumable-stream package
  2. Stream tracking: Your persistence layer saves the activeStreamId in the chat data
  3. Client reconnection: When the client reconnects (page reload), the resume option triggers a GET request to /api/chat/[id]/stream
  4. Stream recovery: The GET handler checks for an activeStreamId and uses resumeExistingStream to reconnect. If no active stream exists, it returns a 204 (No Content) response
  5. Completion cleanup: When the stream finishes, the onFinish callback clears the activeStreamId by setting it to null

Customize the resume endpoint


By default, the useChat hook makes a GET request to /api/chat/[id]/stream when resuming. Customize this endpoint, credentials, and headers, using the prepareReconnectToStreamRequest option in DefaultChatTransport:

app/chat/[chatId]/chat.tsx

import { useChat } from '@ai-sdk/react';import { DefaultChatTransport } from 'ai';
export function Chat({ chatData, resume }) {  const { messages, sendMessage } = useChat({    id: chatData.id,    messages: chatData.messages,    resume,    transport: new DefaultChatTransport({      // Customize reconnect settings (optional)      prepareReconnectToStreamRequest: ({ id }) => {        return {          api: `/api/chat/${id}/stream`, // Default pattern          // Or use a different pattern:          // api: `/api/streams/${id}/resume`,          // api: `/api/resume-chat?id=${id}`,          credentials: 'include', // Include cookies/auth          headers: {            Authorization: 'Bearer token',            'X-Custom-Header': 'value',          },        };      },    }),  });
  return <div>{/* Your chat UI */}</div>;}

This lets you:

  • Match your existing API route structure
  • Add query parameters or custom paths
  • Integrate with different backend architectures

Important considerations


  • Incompatibility with abort: Stream resumption is not compatible with abort functionality. Closing a tab or refreshing the page triggers an abort signal that will break the resumption mechanism. Do not use resume: true if you need abort functionality in your application
  • Stream expiration: Streams in Redis expire after a set time (configurable in the resumable-stream package)
  • Multiple clients: Multiple clients can connect to the same stream simultaneously
  • Error handling: When no active stream exists, the GET handler returns a 204 (No Content) status code
  • Security: Ensure proper authentication and authorization for both creating and resuming streams
  • Race conditions: Clear the activeStreamId when starting a new stream to prevent resuming outdated streams

View Example on GitHub

On this page

Chatbot Resume Streams

How stream resumption works

Prerequisites

Implementation

1. Client-side: Enable stream resumption

2. Create the POST handler

3. Implement the GET handler

How it works

Request lifecycle

Customize the resume endpoint

Important considerations

Deploy and Scale AI Apps with Vercel.

Vercel delivers the infrastructure and developer experience you need to ship reliable AI-powered applications at scale.

Trusted by industry leaders:

  • OpenAI
  • Photoroom
  • leonardo-ai Logoleonardo-ai Logo
  • zapier Logozapier Logo

Talk to an expert