The abandoned run
Closing a tab only closes a connection: the generation usually keeps going, nothing in the browser decides otherwise, and no provider documents what it charges you for the rest — so somebody has to choose, on purpose, what an abandoned run costs and who stops it.
Someone asks the chatbot for award availability, watches for forty seconds, decides they will deal with it later, and closes the tab. The model has another fifty seconds of work to do.
Nothing in the browser has any opinion about that. The connection is gone; the run is not. Somebody, at some point, has to decide what happens next, and the default is that nobody does.
What is documented
The mechanism half of this is established, by Vercel, describing the default wiring of its own SDK:
“Client-side aborts are treated as disconnects. Closing a tab, refreshing the page, or calling
stop()only closes the current HTTP connection and should not cancel the underlying generation.”
That is Vercel documenting Vercel, and it describes a default rather than a law. A backend that forwards the disconnect into a cancellation on the provider call does stop generating. The claim to hold is “unless your server wires it up,” not “cannot.”
The browser side is silent in a way worth stating precisely. MDN documents that abort() “is able to abort fetch requests, the consumption of any response bodies, or streams” — all client-side effects, all about what stops being read. MDN never says whether the server stops producing, in either direction. Do not fill in that silence; it is the premise of the stop-button lesson and it is the reason this one exists.
What is not documented, and the honest way to say so
Here is the part most writing on this subject gets wrong, including the line this course originally registered for this lesson.
“The tokens keep being billed” feels like it follows from “the generation keeps going.” It is not documented by either provider. This course went looking, on 2026-09-05, in the places that would have to contain it: Anthropic’s errors page, fetched in full, which covers status codes, retries, request-size limits and long-request guidance in detail; Anthropic’s streaming page; the Messages API reference; and OpenAI’s own streaming guide. None of them addresses billing when the client of a streaming request disconnects.
What the search did turn up was a third-party aggregator’s support article and threads on a vendor community forum. Neither is a primary source. An aggregator describes its own resale behaviour, and a forum thread is somebody guessing in public with better formatting than yours.
Where people get burned
The trap here is subtle enough that it is worth naming as a habit rather than a fact. When a mechanism is documented and its obvious consequence is not, the consequence tends to get quoted as though the same source supplied it. That is how “Vercel says generation continues” becomes “Anthropic says you are billed for it” in three retellings, and the third person to hear it will repeat it in a budget meeting.
Softening it does not help. “Providers generally bill for abandoned runs” is the same unsourced claim with hedging on top. Either label it as an inference or measure it yourself, which is what the task below does.
Four policies, and only one of them is free
The decision is small once you accept that no page will make it for you. There are four positions, and each one is defensible for a different surface.
- Let it finish, and keep the result. The run completes into the durable store you built in the two-tabs-one-run lesson, so a user who comes back finds their answer waiting. This turns the abandoned run from waste into a cache, and it is the only policy that gets something back for the compute. It costs storage and a retention decision.
- Cancel on disconnect. The server notices the client is gone and propagates a cancellation into the provider call. Cheapest on compute, and it forecloses resumability entirely — a refresh two seconds later has nothing to reattach to. Pick this when answers are short and cheap to redo.
- Cap it. Let the run continue without a client, but only up to a limit you set: a wall-clock budget, a token budget, or both. The middle position, and usually the right one for a long advisory answer. It requires the cap to be enforced server-side, where the run is, rather than in the interface.
- Inherit the default. Which is: it runs to completion, nothing keeps the output, nobody counts it. This is the only option with no design work, and it is what every product does before someone reads this lesson.
Note that the first three are not exclusive, and that all three require the same underlying capability — a server that knows a run exists independently of any connection. You built that in the previous lesson. This one is deciding what to do with it.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
You choose “cancel on disconnect” and wire the server’s abort into the provider call. Name two things you have just broken.
Check your answer
Refresh survival. A refresh is a disconnect. Under this policy the run the user is about to reattach to is killed by the act of reloading, so the resumability work from the previous lesson now has nothing to resume. If you want both, the server has to distinguish “client went away” from “client asked to stop,” and only the second is an instruction.
The second viewer. If one of two tabs closes, a naive disconnect handler cancels a run somebody else is still watching. The run’s lifetime is now a function of how many readers it has, not of one connection — which is the same reframing as the previous lesson, arriving as a bug this time.
Both failures come from the same mistake: treating a transport event as a user intention. A closed connection is an observation. A stop button is a decision. Only one of them should cancel work.
Hands on
Decide what an abandoned run costs, then measure it yourself
Done when: ARTIFACT.md states the abandoned-run policy for the flight chatbot in writing — which of the four, why, and what enforces it — and records one measurement you took on your own provider account for a run you abandoned deliberately.
- Write the policy first, before measuring, so the number does not quietly choose it for you. One paragraph: which of the four positions, what it costs, what it forecloses, and what enforces it server-side.
- Instrument your own server so a run records its own start, end and reported usage independently of the connection. This is where you find out whether your backend even notices a disconnect, which is worth knowing on its own.
- Run the experiment the documentation refuses to answer. Send a long request, abandon it at a known point, and compare what your provider account reports for that run against an identical request you let finish. This is your account, your data, and the only primary source available on the question.
- Record the result with the date, the provider, and the exact way you abandoned it. Then write the caveat next to it: this is one observation on one account on one date, which is a measurement rather than a documented guarantee, and it can change without anyone telling you. That framing is the point of the step.
- Implement the policy and prove it. If you capped the run, exceed the cap and watch it stop. If you cancel on disconnect, confirm a refresh does not trigger it. If you let runs finish into storage, abandon one and confirm the answer is waiting on return.
- Bring the policy paragraph and the measurement into the chat. I will argue for a different one of the four and you will need the trade, not the preference.
What this does not cover
The mechanism for reaching the server from a stop button — and what state a deliberate stop leaves on both sides — is the stop-button lesson in the control module. This lesson is about the case where nobody pressed anything, which is the harder one precisely because there is no user intention to honour.
It also has nothing to say about what rendering costs. Compute spent on a run nobody is watching and compute spent re-rendering a conversation somebody is watching are different budgets with different owners; the second belongs to Front-end performance under streaming load, in full.
That closes the course. What you have now is the stream, the partial answer, the control surface and the run’s lifecycle — and one page worth keeping open long after the lessons are done. The transport reference table is the decision you will actually have to make again, on somebody else’s infrastructure, without this course in front of you.
Read this next — primary source
Errors — Claude APIAnthropic — vendor documenting its own API. Fetched in full 2026-09-05
Read this page in full for the reason it is cited here, which is unusual: it is thorough. Status codes, error types, request-size limits, SDK retry behaviour, long-request guidance, and the structural warning that an error can arrive after a 200. Then notice what a page this complete still does not say — anything at all about what happens to the bill when the client of a streaming request disconnects. Reading a good source until you can state precisely where it stops is the skill this lesson is actually teaching, and this page is the best available specimen to practise on.
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.