Claude API Partial Response Cancellation Guide
When a user stops reading, closes a tab, or clicks "stop generating" halfway through a long Claude response, you need a way to cancel that request before it finishes — without leaking connections, wasting tokens, or leaving your UI in a broken state. This is partial response cancellation: terminating a streaming Claude API call mid-generation while handling the already-received tokens correctly.
The short answer: Claude's API doesn't have a dedicated "cancel" endpoint. Cancellation happens on the client side by aborting the HTTP connection (via AbortController in JavaScript, or closing the socket in other languages). Anthropic stops generating tokens once it detects the connection is closed, and you're typically only billed for tokens actually generated up to that point, not the full response you would have received. The rest of this article covers how to implement that correctly, what edge cases to watch for, and how billing and partial-content handling work in practice.
Why Cancellation Matters
Long-form Claude responses — multi-paragraph explanations, generated code, structured reports — can take 10-30 seconds or more to fully stream. Without cancellation support:
- Users who navigate away still consume your server's streaming resources
- You pay for tokens nobody reads
- Reused HTTP connections can get stuck in a bad state
- Retrying a cancelled request from scratch wastes the tokens already generated
Supporting cancellation is a correctness issue as much as a UX one, especially if you're running many concurrent streams per server.
How Cancellation Actually Works
Claude's streaming API (Server-Sent Events over HTTP) generates tokens and pushes them down an open connection. There's no "stop job #123" API call. Instead:
- You start a streaming request.
- You keep a reference to the thing controlling that connection (an
AbortController, a cancellablefetch, a Python request with a timeout/close hook). - When the user wants to stop, you call
.abort()or close the underlying socket. - The server sees the client disconnect and stops generating further tokens for that request.
This means cancellation is fundamentally a client-initiated connection close, not a server-side cancel command.
JavaScript Example (Browser/Node)
const controller = new AbortController();
async function askClaude(prompt) {
const response = await fetch("https://api.subtoapi.app/v1/messages", {
method: "POST",
headers: {
"Authorization": `Bearer ${SUBTOAPI_KEY}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
model: "claude-sonnet-4",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: prompt }]
}),
signal: controller.signal
});
const reader = response.body.getReader();
let partialText = "";
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
partialText += new TextDecoder().decode(value);
// update UI incrementally here
}
} catch (err) {
if (err.name === "AbortError") {
console.log("Stream cancelled. Partial text so far:", partialText);
} else {
throw err;
}
}
}
// Call this when the user clicks "Stop"
function cancelGeneration() {
controller.abort();
}
The key detail: wrap your stream-reading loop in a try/catch and check for AbortError specifically. Don't let the abort surface as an unhandled exception — it's an expected event, not a failure.
Server-Side Proxies
If your frontend talks to your own backend, which in turn calls the Claude API, you need to propagate cancellation across both hops. When the frontend disconnects (e.g. closes the SSE connection to your server), your server needs to detect that and abort its own upstream request to Claude. In Node.js with Express, listen for the close event on the incoming request and call .abort() on the outgoing fetch's controller. Skipping this step is the most common bug: the frontend gives up, but the backend keeps burning tokens because nothing told it to stop.
What Happens to Billing on a Cancelled Stream
You're generally billed for the output tokens actually generated and streamed to you before cancellation, not the full response that would have completed. If you cancel after 200 tokens of a potential 800-token response, you're charged for roughly 200 tokens of output, plus your input tokens as normal. This is one of the practical reasons to implement cancellation properly — it directly reduces API spend on abandoned requests, especially in chat UIs where users frequently interrupt long answers.
If you're routing calls through SubToAPI, usage metadata returned with each request reflects actual tokens consumed, so cancelled streams show up accurately in your per-key and per-seat usage reporting rather than being counted as if they completed.
Handling Partial Content Correctly
A cancelled response isn't garbage — it's a legitimate partial result, and you should treat it as one:
- Store what you have. If you're persisting conversation history, save the partial assistant message with a flag like
"cancelled": truerather than discarding it. - Don't retry the whole prompt. If the user wants the rest, consider sending a follow-up like "continue from where you left off" rather than regenerating from scratch.
- Close resources cleanly. Release readers, timers, and any UI "generating..." indicators as part of the same cancellation handler, not as an afterthought.
- Debounce rapid cancel/retry clicks. Users who double-click "stop" and then "regenerate" quickly can cause overlapping requests if you don't guard against it with request IDs or a simple lock.
Testing Cancellation Behavior
Before shipping, deliberately test:
- Cancelling within the first token (connection barely open)
- Cancelling mid-stream (hundreds of tokens in)
- Cancelling right as the final token arrives (race condition between "done" and "abort")
- Network drop vs. explicit user cancel (both should behave the same way client-side)
If you're building this on top of SubToAPI, the streaming behavior and SSE format follow the same conventions documented in /docs/streaming, so standard AbortController patterns work without modification — you don't need special handling for sub_live_ keys versus any other bearer token.
Quick Reference
- Claude's API has no server-side "cancel job" endpoint — cancellation is a client-side connection abort
- Use
AbortControllerin JavaScript, or the equivalent timeout/close mechanism in your language - Propagate cancellation through any backend proxy layer, not just the frontend
- You're billed for tokens generated up to the cancellation point, not the full intended response
- Treat partial responses as valid data, not errors
Questions
Does cancelling a Claude API stream stop billing immediately? Billing reflects tokens actually generated before the connection closes. Once you abort, no further output tokens are produced or charged, though there may be a small amount of in-flight token generation between your abort signal and the server detecting it.
Can I cancel a non-streaming (single-response) Claude API call? Yes, in the same way — close the connection before the response completes. But since non-streaming calls return nothing until finished, there's no partial content to keep, so the main benefit is just freeing up the connection, not saving tokens mid-generation.
Why does my cancelled request still show as "completed" in logs? This usually means the abort signal didn't propagate past your frontend to the actual upstream request to Claude. Check that any backend proxy also aborts its own outbound call when the client disconnects, rather than letting it run to completion silently.