Surviving a refresh
Resumability is not a nice-to-have on a two-minute response — it requires the stream to exist somewhere other than the connection, which is an infrastructure decision the UI cannot make on its own.
Ninety seconds into a two-minute answer about transferring points to an airline partner, the user hits reload. Or their phone locks and Safari discards the tab. Or they follow a link, read something, and come back.
Today the answer is gone. Not stopped, not failed — gone, with an empty composer and no evidence anything ever happened. The user asks the same question again and waits another two minutes for an answer that was already most of the way to being written.
A refresh is not a stop
Start with the one sentence that reframes the whole problem. Vercel documents its own default wiring:
Vercel writes the AI SDK and sells the platform this page leads to, so the scope of the claim is their wiring rather than streaming HTTP in general. Within that scope it says something useful: the answer is not lost. Your connection to it is. The generation is still happening, in a process that has no idea the reader left, and the reason the screen is empty is that nothing was holding the output anywhere except the socket you just closed.
That is the reframe. Resumability is not a recovery feature bolted onto a failure. It is what you get automatically once the stream exists somewhere other than the connection, and everything difficult about it is in that one requirement.
What the requirement actually costs
Vercel’s own documented architecture is the honest price list, and it is worth reading precisely because the vendor has every incentive to make this look cheap. The page requires Redis, a persistence layer tracking an activeStreamId per chat, two endpoints, and the resumable-stream package.
Three of those four are yours to build and operate. The package is a dependency; the durable store is infrastructure somebody has to run, the persistence layer is code somebody has to write, and the second endpoint is a route somebody has to secure. Read the ordering constraint too: the stream id has to be written before the first token, because an id that is recorded when generation finishes cannot help a client that disconnected in the middle.
Redis is Vercel’s choice, not a requirement of the problem. The constraint is a durable, queryable store keyed by a stream or run id, readable by a process other than the one that started the generation. Anything satisfying that satisfies this. What you cannot do is skip it: the documentation is explicit that the persistence layer is something you build, not something the SDK provides.
Where people get burned
The tempting shortcut is to persist the assistant message as it streams, then on reload read the last row and show it. That gets you a transcript, not a resume. The user sees ninety seconds of text sitting still, with no way to know whether it is finished, dead or about to continue — and no continuation ever arrives, because nothing reattached to the live generation. Replaying a prefix and resuming a stream are different features, and shipping the first while describing it as the second is how a “fixed” refresh bug comes back as “the answer sometimes just stops.”
The client half
With the infrastructure in place the client side is small, which is exactly the wrong lesson to take from it. useChat returns a named resumeStream function, and Vercel’s documented pattern calls it on mount:
// Server: persist a stream id the moment generation starts, independent
// of any one HTTP connection.
// Client:
const { resumeStream } = useChat()
useEffect(() => {
resumeStream() // attempts to reattach to an in-flight generation, if any
}, [])Note the word attempts. There may be nothing to reattach to: the generation finished while the tab was closed, or it never started, or the id expired. Every one of those needs a rendering, and none of them is the same screen as “reattached and streaming.” A reload that silently shows nothing while a resume attempt fails is the original bug wearing a dependency.
Four behaviours, and you have to pick one
“Correct and stated” means choosing from this list on purpose, per surface:
- Lose it. The refresh discards the turn and the composer comes back empty. Cheapest, and defensible on a surface where answers are short and cheap to regenerate. It stops being defensible at two minutes.
- Replay the prefix. Show what was persisted, clearly marked as incomplete and not continuing. Honest, cheap, and useful on a surface where the partial answer has standalone value. Do not describe it as resuming.
- Reattach. Replay the prefix and continue live from the generation that never stopped. The real thing, and the one that costs the store, the id and the second endpoint.
- Restart. Throw the partial away and generate again from the top. Simple and correct, and it pays for the whole answer twice while the user watches text they have already read.
The choice is not purely a UX one, which is the point of putting it in this module. Reattach requires infrastructure the UI team does not own. Restart doubles a cost somebody is paying. Lose-it is free and gets expensive in complaints. Whoever is designing the surface has to bring the trade to the people who own the other two variables rather than picking whichever the framework did by default.
What the durable store now contains
One consequence teams meet late. Before resumability, in-flight assistant output existed only in transit and in the browser. After it, every partial answer lands in a store with a retention policy, an access control list and a backup schedule, and it is user content — for this chatbot, questions about somebody’s points balances and travel plans.
This course is not going to tell you what the retention window should be; that depends on rules it has not verified for your organisation. What it will tell you is that adding resumability adds a new store of user content, and that the people who need to know are the ones who would have found out during a review six months later. Raise it in the same conversation as the five questions above.
Retrieval check
A team says refresh is “handled” — they persist assistant text as it streams and render it on reload. What have they actually shipped?
Check your answer
A transcript. On reload the user sees the text that had arrived before the disconnect, and it never moves again, because nothing reattached to the generation that is still running somewhere without a reader.
The tell is that their fix required no stream id, no second endpoint and no store that a different process reads. Resumption needs a live generation to be findable by something other than the connection that started it. Persisting text as it arrives makes the past readable and does nothing about the future, so the surface has traded “the answer vanished” for “the answer stops mid-sentence and looks finished,” which is worse on an advisory product.
Check your recall
Answer from memory — no scrolling back.
Hands on
Decide, then ship, what a refresh does to the flight chatbot
Done when: Reloading the tab mid-answer produces one of the four behaviours, chosen deliberately and written in ARTIFACT.md — and if the choice is reattach, a reload during a long answer restores the prefix and continues to receive new tokens.
- Find out what happens today. Ask a question with a long answer, reload at roughly the halfway point, and write down exactly what the screen does. Do not guess this from the code.
- Check whether the generation survived. With the tab reloaded, watch your provider usage or your own token counter for thirty seconds. If it is still climbing, the answer was never lost and you are only missing a way back to it.
- Pick one of the four behaviours and write the sentence in
ARTIFACT.md. Losing it is an acceptable answer here if you can defend it against a two-minute response. Not choosing is not. - If you chose reattach, put the five questions to whoever owns the backend — what the id is, where it lives, how long, who can read it, what ends it — and record the answers before you write any client code. If you own the backend, answer them in writing anyway.
- Build it, then test the three failures rather than the success: reload after the generation has already finished, reload when there was never a stream, and reload after the id has expired. Each needs a rendering that is not a blank answer area.
- Record the retention window you chose for stored partial answers and who you told. This is the step that gets skipped and the one somebody asks about later.
What this does not cover
This lesson solves one reader coming back to one run. The same stream id makes a second viewer and a mid-flight reconnect the same problem with the same answer, and Vercel’s worked example demonstrates the single-reader case rather than fanning out to two live viewers, so treat concurrency as implied by the architecture and not shown by the source. That is the two-tabs lesson in the production module.
Everything between your server and the browser that decides whether a stream arrives incrementally at all — proxies, gateways and CDNs that buffer by default — is the infrastructure-survival lesson in that same module. There is no point resuming a stream into a proxy that holds it.
The case where the connection dies and you have no durable store to reattach to is the retry lesson before this one. Correcting an answer that arrived intact and answered the wrong question is the partially-wrong-answer lesson next. And what an unread generation costs while it runs on without a reader is the abandoned-run lesson, which is where this module’s uncomfortable question finally gets asked directly.
Read this next — primary source
Chatbot Resume StreamsVercel — vendor documenting its own product (AI SDK) and the platform it runs on. Free. Re-verified September 5, 2026, wording unchanged since the September 2–3 pass
The most complete published account of what resumability actually costs, from a vendor with an interest in making it look easy — which is what makes the length of its prerequisite list persuasive. Read the whole page for the two endpoints, the stream id that has to be written before the first token, and the ordering constraints between them. Read it as one worked architecture rather than the only one: Redis and the resumable-stream package are its choices, and the requirement underneath them is any durable store keyed by a stream id.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.