Claude API Streaming: How to Cancel a Response Mid-Stream
Can you cancel a Claude API streaming request?
Yes. When you stream a response from the Claude API, the request stays open as an HTTP connection (typically Server-Sent Events) until the model finishes generating or you close the connection yourself. Canceling is a client-side operation: you abort the underlying HTTP request, the server stops sending tokens, and Claude stops generating for that request. There's no separate "cancel" endpoint to call — you just close the stream.
This matters in a few common real-world scenarios: a user navigates away from a chat UI mid-response, clicks a "stop generating" button, or you have a race condition where you kick off a second request and want to kill the first. In all of these cases, the fix is the same pattern used across modern fetch-based APIs: AbortController on the client, and proper cleanup on your server if you're proxying the stream.
Why cancellation matters for streaming
Streaming responses are long-lived connections. If you don't cancel them properly:
- You keep paying for tokens the user never reads, since billing is based on tokens generated, not tokens displayed.
- You leak server resources — open sockets, unresolved promises, abandoned generators — especially in Node.js backends proxying the stream to a browser.
- You can hit concurrency limits if abandoned streams count against your account's concurrent-request cap.
Getting cancellation right is not optional for production chat UIs. It's the difference between a responsive "stop" button and a backend quietly accumulating zombie connections.
Canceling with AbortController (Node.js / browser)
The standard mechanism in modern JavaScript is AbortController. You create a controller, pass its signal into your fetch call, and call .abort() whenever you want to stop.
const controller = new AbortController();
async function streamResponse(prompt) {
const res = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
headers: {
"x-api-key": process.env.ANTHROPIC_API_KEY,
"anthropic-version": "2023-06-01",
"content-type": "application/json",
},
body: JSON.stringify({
model: "claude-3-5-sonnet-20241022",
max_tokens: 1024,
stream: true,
messages: [{ role: "user", content: prompt }],
}),
signal: controller.signal,
});
const reader = res.body.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
process.stdout.write(new TextDecoder().decode(value));
}
}
// Call this from a "stop" button handler
function stopGenerating() {
controller.abort();
}
When abort() is called, the fetch promise rejects with an AbortError, the reader's read() call throws, and the underlying TCP connection closes. You should wrap the read loop in a try/catch to handle the abort cleanly instead of letting it bubble up as an unhandled rejection:
try {
await streamResponse(prompt);
} catch (err) {
if (err.name === "AbortError") {
console.log("Stream canceled by user");
} else {
throw err;
}
}
Canceling in Python with httpx
If you're streaming server-side with httpx, you cancel by exiting the streaming context manager or canceling the enclosing asyncio task:
import httpx
import asyncio
async def stream_and_cancel(prompt, cancel_event):
async with httpx.AsyncClient() as client:
async with client.stream(
"POST",
"https://api.anthropic.com/v1/messages",
headers={"x-api-key": "...", "anthropic-version": "2023-06-01"},
json={
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"stream": True,
"messages": [{"role": "user", "content": prompt}],
},
) as response:
async for chunk in response.aiter_bytes():
if cancel_event.is_set():
break # exits the context manager, closes the connection
print(chunk)
Breaking out of the async for loop while inside the async with block closes the HTTP connection on the way out, which is functionally the same as aborting on the client side.
Server-side proxy pattern: forwarding cancellation
If your frontend talks to your own backend, which in turn streams from Claude, you need to propagate the cancellation in both directions. The common pattern:
- Browser sends a request with its own
AbortControllersignal to your backend. - If the browser aborts (tab closed, button clicked), your backend's request handler should detect the client disconnect (e.g., Express's
req.on("close")). - Your backend then aborts its own outbound fetch to Claude using a second
AbortController.
app.post("/chat", async (req, res) => {
const upstreamController = new AbortController();
req.on("close", () => {
upstreamController.abort();
});
const upstream = await fetch("https://api.anthropic.com/v1/messages", {
method: "POST",
signal: upstreamController.signal,
// ...headers, body
});
upstream.body.pipe(res);
});
Without this chain, closing the browser tab doesn't actually stop Claude from generating — your backend will keep consuming (and paying for) tokens even though nobody's listening.
How SubToAPI handles this
If you're using SubToAPI (https://subtoapi.app) to expose your Claude access as an HTTPS API with sub_live_... keys, the same AbortController pattern applies directly — SubToAPI's streaming endpoint is a standard SSE stream, so aborting the fetch on your side closes the connection and stops billing for that request immediately. See /docs/streaming for the exact request shape and /docs/messages for the non-streaming equivalent if you'd rather poll for completion. If you're just getting started, /docs/quickstart walks through generating your first key and making a request; pricing details for Solo, Team, and Scale plans are at /pricing.
The key point: cancellation is always a connection-close operation, whether you're calling Claude directly or through a proxy like SubToAPI. There's no cancel token or cancel API call to learn — just abort the HTTP request.
FAQ
Does canceling a stream stop token billing immediately? Yes, in practice. Once the connection closes, the model stops generating and you're only billed for tokens produced up to that point, not for the full response you would have received.
Can I cancel just one request if I have multiple streams open? Yes, as long as each request has its own AbortController (or equivalent). Calling abort() on one controller only closes that specific connection, leaving other concurrent streams unaffected.
What happens to partial output after I cancel? Whatever tokens were already sent to your client before cancellation remain in your buffer — Claude doesn't retract them. You decide whether to discard, display, or save that partial response in your application logic.