File: chatbot-resume-streams.md | Updated: 11/15/2025
Menu
v5 (Latest)
AI SDK 5.x
Model Context Protocol (MCP) Tools
Copy markdown
==========================================================================================================
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.
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:
resume option in useChat that automatically reconnects to active streamsconsumeSseStream callbackYou build:
resumable-stream
to manage Redis storageTo implement resumable streams in your chat application, you need:
resumable-stream package - Handles the publisher/subscriber mechanism for streamsUse 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.
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 }); }, });}
Create a GET handler at /api/chat/[id]/stream that:
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.

The diagram above shows the complete lifecycle of a resumable stream:
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 packageactiveStreamId in the chat dataresume option triggers a GET request to /api/chat/[id]/streamactiveStreamId and uses resumeExistingStream to reconnect. If no active stream exists, it returns a 204 (No Content) responseonFinish callback clears the activeStreamId by setting it to nullBy 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:
resume: true if you need abort functionality in your applicationresumable-stream package)activeStreamId when starting a new stream to prevent resuming outdated streamsOn this page
1. Client-side: Enable stream resumption
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: