1. Topics
  2. /
  3. AI Stack
  4. /
  5. Vercel AI SDK resumable-stream: what it covers and what it doesn't
11 min readPublished May 6, 2026

Vercel AI SDK resumable-stream: what it covers and what it doesn't

TL;DR Vercel's resumable-stream library covers one scenario: a full page reload while generation is in progress. Tab switches, mobile backgrounding, and device switches all drop the SSE connection with no way to resume. resumable-stream also has a documented incompatibility with stop(). The fix is a session layer where continuity is a property of the channel, not the connection.

resumable-stream ships as part of Vercel's AI SDK infrastructure. It is well-named for what it does: resume a stream after a full page reload. For production applications with more complex continuity requirements, it covers one scenario well and is explicit about the rest - and understanding exactly where it applies is the first step to building streaming that holds up.

Copy link to clipboard

What resumable-stream does

resumable-stream is a Redis-backed library that buffers token output server-side. When a client reconnects after a full page reload, it reads from the Redis buffer and replays tokens from where it left off. Generation continues uninterrupted on the server. The client catches up.

This works for the specific case it was designed for: a user reloading the page during a long generation. Vercel published the library specifically to address this scenario. For page reloads, it does what it says.

Copy link to clipboard

What resumable-stream doesn't cover

It handles the page-reload case reliably. There are several scenarios it doesn't cover, and the documentation is explicit about them.

Tab switches. When a user switches tabs, the browser may suspend or deprioritize the tab. The SSE connection drops. When the user switches back, there is no resume - the stream is gone. resumable-stream does not handle tab switches. This is explicit in the documentation.

Mobile backgrounding. iOS and Android aggressively terminate background network connections. An AI response in progress when a user switches apps is lost. There is no recovery path.

Device switches. A user who starts a conversation on a laptop and picks up on their phone gets a new session. The in-progress generation is not accessible from the second device. SSE is scoped to a single HTTP request from a single client.

Multiple tabs. A second browser tab opened during an active generation starts an independent session. It has no access to the existing stream.

Network changes. Moving from Wi-Fi to mobile data, or any network transition that drops the connection, loses the stream. There is no reconnection protocol at the SSE level.

These failure modes are common across mobile and web contexts. None of those sessions recover automatically with resumable-stream.

For a practical self-audit of which of these gaps apply to your application, see Vercel AI SDK in production: when DefaultChatTransport needs a session layer.

Copy link to clipboard

The abort incompatibility

resumable-stream has a second limitation you'll hit if you're using it: it is not compatible with stop().

When a user cancels a generation, resumable-stream treats the abort as a disconnect and attempts to resume. This creates a conflict: the user stopped intentionally; the library tries to continue. Vercel acknowledged this in GitHub issue #8390: "You are correct. Right now, resume and stop are not compatible."

The issue remains open. If you're using resumable-stream, you have to choose between resumability and reliable cancellation - the library cannot provide both.

Copy link to clipboard

Why SSE makes this structural

These aren't gaps that a newer version of resumable-stream can close. They follow from how SSE works.

SSE is an HTTP connection from one client to one server. The connection carries no session identity at the protocol level - it is the session. When the connection drops, the session ends. A new connection is a new session.

The Redis buffer in resumable-stream gives the server a way to store output between connections. But it doesn't give the client a way to reconnect to a specific session. The reconnection depends on the client presenting the right session ID to the right server endpoint. That works on reload, where the page logic handles it. On a tab switch or device change, there is no automatic reconnect mechanism - so it fails.

This is why the maintainer pointed to WebSockets as the real solution in issue #6502. WebSockets are persistent and bidirectional. A client that disconnects can reconnect explicitly, request history from a specific offset, and resume. SSE cannot do this by design.

Copy link to clipboard

What session continuity actually requires

The property you need is: session continuity as a feature of the session, not the connection. Any client with the right session identifier - same tab, different tab, different device, after a mobile background - should be able to join and catch up.

This requires three things that resumable-stream doesn't provide:

Offset-based history. Every token needs a position. A reconnecting client presents its last known offset; the server replays from there. Without offsets, the client has to receive everything from the start or nothing.

Session fan-out. Multiple clients subscribing to the same session should all receive the same stream. SSE cannot do this - each client gets its own connection and its own session.

Reconnect semantics. The session layer needs to distinguish a reconnecting client from a new client. On reconnect, it replays missed tokens. On new session, it starts fresh.

A channel-based transport provides all three natively. Session continuity is a property of the channel, not the connection. Any client that connects to the same channel - same tab after a switch, different device, monitoring dashboard - gets current state and replays from the offset it last received.

Vercel's ChatTransport interface in AI SDK 5 is the integration point. A channel-based transport plugs in at this point. useChat hooks, application logic, and UI rendering stay exactly as they are:

const { messages } = useChat({
  transport: new ChannelBasedTransport({ sessionId })
});

The transport manages reconnection, offset tracking, and fan-out. Your application doesn't need to know which device or tab is connected.

Copy link to clipboard

The Redis path vs the Ably path

The Vercel AI SDK stream resumption guide documents a Redis-backed implementation for the page-reload case. It works in production. The consideration is what building it fully involves: three custom endpoints, a Redis dependency, and cancellation logic you implement yourself. On the other side of the comparison: a transport swap and one server-side call.

Copy link to clipboard

What the Redis implementation involves

The implementation requires three custom endpoints: POST /api/chat to create and publish the stream to Redis, GET /api/chat/[id]/stream to replay it for reconnecting clients, and POST /api/chat/[id]/stop to handle cancellation. The stop endpoint is the one most implementations skip.

POST /api/chat generates the stream and writes it to Redis. Inside the consumeSseStream callback, you create a stream context using createResumableStreamContext({ waitUntil: after }), assign a unique ID to the stream, and publish it with createNewResumableStream. You save that ID to a database column on the chat record and clear it in onFinish.

async consumeSseStream({ stream }) {
  const streamId = generateId();
  const ctx = createResumableStreamContext({ waitUntil: after });
  // Publish stream to Redis under a stable ID
  await ctx.createNewResumableStream(streamId, () => stream);
  // Track in your own database
  saveChat({ id, activeStreamId: streamId });
},
onFinish: ({ messages }) => {
  saveChat({ id, messages, activeStreamId: null }); // clear on completion
}

GET /api/chat/[id]/stream replays the stream for a reconnecting client. When useChat mounts with resume: true, it hits this endpoint automatically. The endpoint can return 204 No Content when no active stream exists, so the SDK knows there is nothing to resume. Any other status is treated as an error.

if (chat.activeStreamId == null) {
  return new Response(null, { status: 204 }); // not 200, not 404
}
const ctx = createResumableStreamContext({ waitUntil: after });
return new Response(
  await ctx.resumeExistingStream(chat.activeStreamId),
  { headers: UI_MESSAGE_STREAM_HEADERS }, // required constant from 'ai'
);

POST /api/chat/[id]/stop is a dedicated cancellation endpoint you build from scratch. stop() from useChat closes the HTTP connection, not the generation. Without this endpoint, the server keeps running inference after the client leaves.

The handler requires three idempotency guards. First, check that activeStreamId is not null before acting. Second, confirm the client's activeStreamId matches the server's current one, since a newer stream may have started after the stop was dispatched. Third, re-read the database before clearing the ID to prevent a race condition between the stop and a new generation starting. cancelActiveWork() and markStreamAsStopped() are placeholders in the SDK docs. You implement them against your own backend.

// Guard 1: nothing active
if (chat.activeStreamId == null)
  return Response.json({ success: true });

// Guard 2: client sent a stale stream ID
if (body.activeStreamId != null && body.activeStreamId !== activeStreamId)
  return Response.json({ success: true });

await markStreamAsStopped(activeStreamId); // your implementation
await cancelActiveWork(activeStreamId);    // your implementation

// Guard 3: re-read before clearing to avoid race
const latest = await readChat(id);
if (latest.activeStreamId === activeStreamId)
  await saveChat({ id, activeStreamId: null });

Alongside the three endpoints, you also need:

  • A Redis instance. Provisioning, TTL configuration, and monitoring are yours to own. resumable-stream sets no default stream expiry.

  • A database column for activeStreamId on each chat record. You own the reading and writing of it.

  • after() from next/server, used as the waitUntil value in the Vercel AI SDK examples. This keeps the Redis write alive after the HTTP response closes. Other runtimes can supply a compatible waitUntil function, but no alternative examples are documented.

Copy link to clipboard

The Ably path

@ably/ai-transport is a custom ChatTransport implementation for the Vercel AI SDK. The ChatTransport interface is how the Vercel AI SDK lets you replace the default HTTP transport. It handles sendMessages and reconnectToStream, and useChat delegates to it. Swapping the transport is a single change to your component:

// Before: default HTTP transport
const { messages, sendMessage, stop } = useChat({ id: chatId });

// After: Ably transport
const { chatTransport } = useChatTransport(); // from <ChatTransportProvider>
const { messages, sendMessage, stop } = useChat({
  id: chatId,
  transport: chatTransport, // drop-in replacement
});

useChat hooks, message rendering, and UI state stay exactly as they are.

The server route has one meaningful change. By default, tokens stream back through the HTTP response. Here, run.pipe() sends them to an Ably channel instead. The HTTP response returns immediately with a run ID, and the client receives tokens through the channel from that point.

const run = session.createRun(invocation, { signal: req.signal });
after(async () => {
  await run.start();
  await run.loadConversation(); // rebuild conversation from channel history
  const result = streamText({
    model,
    messages: await convertToModelMessages(run.messages),
    abortSignal: run.abortSignal, // fires if the client calls stop()
  });
  // Routes to Ably channel, not the HTTP response
  const pipeResult = await run.pipe(result.toUIMessageStream());
  const outcome = await vercelRunOutcome(pipeResult, result.finishReason);
  await run.end(outcome);
});
return Response.json({ runId: run.runId }); // returns immediately

The reconnection and recovery layer handles all of this without any code on your side. For a production walkthrough of stream resumption alongside barge-in and human handover, see AI agent streaming in action . Brief disconnects replay missed messages automatically on reconnect. Longer outages fall back to channel history. Server-side append failures recover silently inside run.pipe(). stop() from useChat cancels the run directly, with no separate endpoint needed.

No GET resume endpoint. No stop endpoint. No Redis instance. No activeStreamId column. Only one dependency is shared with the Redis path: after() from next/server, which keeps the serverless function running after the HTTP response closes.

For the full integration guide, see the Ably AI Transport docs for the Vercel AI SDK. The server sample above covers the stream resumption case. For human-in-the-loop patterns, the guide covers the suspend path alongside it.

Copy link to clipboard

The complexity delta in Redis vs Ably

The Redis implementation is complete and production-deployable. But as this comparison shows, the engineering surface area leaves a lot for you to build and maintain. Three custom endpoints, three idempotency guards in the stop handler, a Redis instance to provision and monitor, a database column to manage, and cancellation logic to implement against your own backend. The Ably path replaces the endpoint trio and the Redis dependency with a transport swap and one server-side call. The operational responsibilities that fall away are not incidental: they are what you would otherwise need to build correctly and maintain over time.

For the build-vs-buy framing at the product level - the four production conditions where Redis-backed resumption compounds infrastructure burden, and the conversations where durable sessions earn their place - see AI chat stream resumption: when Redis is enough, and when you need durable sessions.

Copy link to clipboard

What to look for in a transport

When evaluating transports for session continuity, the relevant capabilities:

Offset-based replay. The transport should track message positions and support catch-up from a specific offset on reconnect - not full replay from the start, which re-triggers the agent.

Multi-device fan-out. One session, multiple subscribers. Connected clients get live streaming; reconnecting clients get automatic catch-up.

Abort compatibility. The transport should support explicit cancel signals as typed messages, separate from connection close. This is the incompatibility resumable-stream cannot resolve.

Ably AI Transport integrates with the Vercel AI SDK to add durable sessions, multi-device sync, and bidirectional control to your chat application. Visit the Ably AI Transport overview, read the documentation, or sign up free

Ready to build? Get started with Vercel AI SDK.

Copy link to clipboard

Frequently asked questions

Copy link to clipboard

Does resumable-stream handle tab switches and mobile backgrounding?

No. resumable-stream handles one scenario: a full page reload during an active generation. When a user switches tabs, the browser may suspend the tab and the SSE connection drops with no mechanism to resume it. The same applies to mobile backgrounding. iOS and Android aggressively terminate background connections. Both limitations are explicit in Vercel's documentation.

Copy link to clipboard

Can I use resumable-stream outside of Next.js?

resumable-stream requires a waitUntil function to keep the serverless runtime alive after the HTTP response closes. The Vercel AI SDK documentation passes after() from next/server to fill this role. Other runtimes can supply their own equivalent - Cloudflare Workers provides ctx.waitUntil, for example - but the Vercel AI SDK documentation only shows the Next.js path. If you are not using Next.js, there is no documented example to follow.

Copy link to clipboard

How do I cancel an AI generation when using resumable-stream?

You need to build a dedicated POST stop endpoint. Calling stop() from useChat only closes the HTTP connection: the server continues running inference until it finishes naturally. The stop endpoint requires three idempotency guards to handle race conditions between a cancellation and a new generation starting. Vercel acknowledged this incompatibility in GitHub issue #8390. The issue remains open.

Copy link to clipboard

How do I deliver the same AI stream to multiple devices or tabs?

SSE is scoped to a single HTTP request from one client. Each tab or device starts its own connection and its own session, with no built-in fan-out. A channel-based transport solves this: session continuity is a property of the channel rather than the connection, so any client with the right session identifier can subscribe and receive the stream, whether on the same device or a different one.


Research basis: analysis of 300+ GitHub issues in vercel/ai repository; 31 Vercel Community Forum threads (65% unresolved); 35 Stack Overflow questions (40% unanswered). GitHub issues cited: #11865 (tab switch, stream resumption), #8390 (resume and stop incompatible - acknowledged, unresolved), #6502 (resumable stream can't be stopped), #8477 (resume + onFinish crashes), #6974, #11512 (Expo/React Native). Maintainer quote from Lars Grammel, Vercel. Vercel acknowledgment from Nico Albanese, Vercel (#8390).


Join the Ably newsletter today

1000s of industry pioneers trust Ably for monthly insights on the realtime data economy.
Enter your email