The stop button
Aborting a fetch stops the browser reading, not the server generating — a stop button that does not reach the server is a UI affordance over a generation that nobody has cancelled and no vendor documents the cost of.
The stop button is the first control you will add to the streaming chatbot and the easiest one in this course to ship broken. You wire a button to controller.abort(). The text stops. The cursor goes away. The composer re-enables. You demo it, nobody asks a second question, and from the outside it is indistinguishable from a stop button that works.
Nothing on the other end of the wire noticed. The model is still generating an award-availability answer that no longer has a reader, the tool call it fired is still running, and your route is still holding whatever it was holding when the socket went quiet.
What abort() is specified to do
Two documents, and it matters which one you lean on for which claim. The normative definition lives in the WHATWG DOM Standard, where AbortController and AbortSignal are actually defined. Its framing of the whole mechanism is the sentence this lesson turns on:
“
AbortControlleris meant to support these requirements by providing anabort()method that toggles the state of a correspondingAbortSignalobject. The API which wishes to support aborting can accept anAbortSignalobject, and use its state to determine how to proceed.”
Read the reach of that. abort() toggles a flag on an object in your page. Something else has to be watching the flag and decide what to do about it. The spec’s conformance requirements in the section on using these objects in APIs bind exactly one population: “Any web platform API using promises to represent operations that can be aborted must adhere to the following.” Web platform APIs. In the page. The spec’s own illustrative abort step reads “Stop doing amazing things,” and the amazing things are assumed to be happening inside the same agent.
For observed browser behaviour rather than conformance language, MDN is the anchor. It documents that abort() “is able to abort fetch requests, the consumption of any response bodies, or streams”. Every verb in that sentence describes your client. Requests it sends. Bodies it consumes. Streams it reads.
const controller = new AbortController()
fetch('/api/chat', { method: 'POST', body, signal: controller.signal })
.then(/* ... */)
// Stop button:
stopButton.onclick = () => controller.abort()
// This stops YOUR client from reading further. It does nothing to the
// server unless your server route itself passes a signal through to the
// provider call and reacts to its cancellation.The silence is the lesson
Nowhere does MDN say whether aborting stops the server producing. Not on the AbortController page, not on AbortSignal.timeout(), not on AbortSignal.any(), and not in the DOM Standard’s abort section. Those four were the search, run when this course’s sources were first checked in early September 2026 and re-checked on the fifth.
The absence is not a gap in the documentation. It is an accurate description of the mechanism’s scope: a browser standard has no jurisdiction over what a process in another data centre does when a socket goes quiet. So the honest sentence is not “aborting does not stop the server.” It is: the platform does not say, and therefore does not promise. Anything you believe past that point has to come from whoever wrote the backend.
What one backend says it does
Vercel documents its own default wiring, and is blunt about it:
Vercel writes the AI SDK, sells the platform this documentation leads to, and is describing what its own default wiring does. That is a vendor documenting its own product, and the claim is exactly that wide: it is not a law of streaming HTTP. The wording is worth noticing too. “Should not cancel” states an intent about their implementation, not a guarantee about yours.
A backend that does listen for the disconnect and propagates a cancellation into the provider call will stop generation. That is the thing you are being asked to build. Say “unless the backend wires the disconnect through to the provider call,” never “cannot.”
Three places a stop has to land
A stop that reaches the server is three separate pieces of wiring, and teams routinely ship the first one alone.
One: the client stops reading. That is the code above, and it is done.
Two: the route notices. The interface exists and is normative — the Fetch Standard declares readonly attribute AbortSignal signal on Request, so a route handler that receives a Request has a signal to listen to. Whether your runtime actually aborts that signal when a client disconnects mid-response is a property of the runtime, not of the specification, and this course has not verified it for any specific one. Verify yours with a log line and a closed tab before you rely on it.
Three: the provider call is cancelled. Noticing is not stopping. The signal has to reach whatever is producing tokens, and the loop around it has to break rather than keep writing into a response body nobody is reading.
// Server route — the shape, not a copy-paste.
export async function POST(request: Request) {
const stream = await callProvider({
// Hand the incoming request's signal down to the thing generating
// tokens. If this argument is missing, steps one and two above are
// decoration.
signal: request.signal
})
request.signal.addEventListener('abort', () => {
// Your own cleanup: mark the run stopped, release the tool, write
// the partial answer or discard it. Decide this on purpose.
})
return new Response(stream)
}The state you leave behind, on both sides
Stopping is not an undo. The half-answer on screen is real, the user read it, and it has to become something. Four decisions, none with a default worth inheriting:
- The partial message. Keep it visible and marked as stopped, or remove it. On an advisory surface a truncated recommendation that looks complete is the worst of the options, so if you keep it, it has to look stopped.
- The conversation history. Does the stopped turn go into the next request’s context? A half-sentence about a transfer partner will be read back as something the assistant said.
- Tool calls in flight. An award search that was running when the user hit stop either completes into nothing or gets cancelled too. Both are defensible. Not knowing which is not.
- Anything you persisted. If you write assistant content to a database as it streams, a stop leaves a row that no completion event will ever close. What that row does to you later is the subject of the retry lesson.
The bill: what nobody documents
The tempting sentence here is that the tokens keep being billed. This course will not write it, because no vendor says it.
Anthropic’s errors page, its streaming page and its Messages API reference were all fetched on September 5, 2026. So was OpenAI’s streaming guide. None of them addresses what happens to billing when the client disconnects mid-generation. Everything that turned up on the question came from third-party aggregators and forum threads, which is not a source for a number that ends up in a budget.
What is documented is narrower and still enough to act on: under at least one vendor’s default wiring, generation continues. Compute is being spent producing output nobody will read. Whether it is metered the way a completed generation is metered is an assumption, and this course labels it as one. Treat an abandoned generation as billable until your provider tells you otherwise in writing, and if the number matters to a business case, ask them and get the answer on paper rather than repeating a blog post.
Retrieval check
Your PM asks whether the stop button saves money. Answer in two sentences without overstating anything.
Check your answer
It saves money only if the server cancels the provider call, because an abort in the browser is documented to stop the client reading and nothing more. Whether an uncancelled generation is billed after the reader disappears is not documented by either major provider, so treat it as billable and treat cancelling it as the only part under your control.
Check your recall
Answer from memory — no scrolling back.
Hands on
Ship a stop button that reaches the server, and prove it
Done when: Stopping the flight chatbot mid-answer measurably stops token production on the provider side, and the four pieces of leftover state — partial message, history, in-flight tools, persisted rows — each have a written decision in ARTIFACT.md.
- Wire the client half first, with an
AbortControllerper turn. Confirm the text stops. This is the part that will fool you, so write down that you do not yet know what the server did. - Add a log line in your route handler on the request signal’s
abortevent. Close the tab mid-answer. If the line never fires, your runtime does not surface client disconnects the way you assumed, and that is the finding — record which runtime it is. - Pass the signal down into the provider call. Not into a wrapper that ignores it. Into the call that produces tokens.
- Prove it. Stop a long answer, then watch your provider’s usage dashboard or your own token counter for thirty seconds. Climbing means a button. Flat means a cancellation. Record the result either way.
- Decide, in writing, what a stop does to the partial message, the conversation history sent with the next turn, any tool call in flight, and anything already persisted. Four lines. Blank is not an answer; “discard it” is.
- Ask your provider, in writing, what an abandoned generation is billed, and paste their answer into
ARTIFACT.md. If they do not answer, record that too — an unanswered question on file beats an assumption in someone’s head.
What this does not cover
Stopping is the deliberate case, where the user chose. A stream that dies on its own at token four hundred leaves the same partial state with nobody having decided anything, and the retry that follows is where duplicated half-answers come from. That is the retry lesson, and it opens on the mechanism you are probably assuming exists.
A stop closes the connection on purpose; a refresh closes it and then asks for it back. Making the answer survive that is the refresh lesson, and the infrastructure it needs is not something the UI can decide alone. Correcting an answer already on screen, rather than stopping it, is the partially-wrong-answer lesson at the end of this module.
What a run costs once nobody is watching, and who is responsible for ending it, is the abandoned-run lesson in the production module. The cost of rendering any of this — the re-renders a stream triggers, the budgets, the measurement — belongs to Front-end performance under streaming load, which owns it in full. This course owns the stream and its lifecycle. That one owns what drawing it costs.
Read this next — primary source
AbortControllerMDN Web Docs — free, community-maintained, non-vendor. Re-checked September 5, 2026
This lesson takes its one hard client-side fact from this page: what abort() is documented to stop. Read it in full for the two things a summary cannot carry — the abort reason, which is how you tell a user-initiated stop from a timeout at the catch site, and the sibling pages for AbortSignal.timeout() and AbortSignal.any(), whose own worked example is a cancel button combined with a deadline. Read it also for what is absent: nowhere on this page, or on any page it links to, does MDN say what happens to the server. That absence is the subject of this lesson.
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.