◉CLAUDE LABJP
●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…●2.1.301 — Adds the vim % motion, LaTeX math rendering in the VS Code panel, and a retry-watchdog limit for usage-limit waits (10/15)●2.1.299 — Conversations over 200K tokens left idle about 54 minutes get compacted; MCP allow rules now match only the server they name●SONNET 4.5 — Retires on the Claude API on 11/30, 45 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●FAST MODE — Opus 5.5 fast mode is out of research preview and no longer needs the beta header (10/15)●NEW — I Put a Ten-Line Promise File at the To…
Articles/API & SDK
⬡ API & SDK/2026-05-02Intermediate

Cancelling Claude API Streams the Right Way: AbortController, Token Billing, and Connection Hygiene

I measured 100 cancelled Claude API streams to find out what actually gets billed. The drift numbers, the $18.70/month a missing close handler costs you, the proxy buffering trap, and a ledger that never loses a usage event — Node.js and Python.

claude-api83streaming22abortcontrollercost-optimization31node-jspython22

✦ Premium Article

The first question I got from a teammate after we shipped a streaming chat UI was: "When the user hits the Stop button mid-response, do we still get billed for what's already generated?" The Anthropic SDK ships a perfectly fine cancellation mechanism, but reading the docs end to end doesn't quite answer "so what shows up on the invoice?" That part you only learn by sending requests, killing them, and inspecting the usage events.

Here's a focused guide to cancelling Claude API streams cleanly with AbortController, what the billing actually looks like when you do, and the connection-pool gotcha that took down our chat endpoint once. Examples are in TypeScript (Node.js) and Python (asyncio), tested against the latest Anthropic SDKs and Claude Sonnet 4.6.

As an indie developer I shipped that chat feature into an iOS app I maintain alone, without ever verifying what a cancelled request costs. What eventually hurt was not the invoice. It was the alert that woke me at 2am when the connection pool ran dry.

Bottom line: what you generate, you pay for

The single sentence to internalize:

  • Input tokens (the prompt) are billed in full the moment the server starts processing — cancelling does not refund them.
  • Output tokens are billed for what was actually generated. If you cut the stream after 12 tokens, you pay for 12.
  • A few tokens may already be in flight on the server when you abort, so expect a small variance (typically 5 to 30 tokens past the visual cut point).

So "I cancelled, therefore zero" is wrong. The right model is "I paid through the cut point, plus a little drift." Anthropic surfaces incremental token counts in the message_delta event's usage.output_tokens field. Capture the most recent value before the abort fires and your accounting will be accurate.

In practice on Sonnet 4.6, an aggressive user who hits Stop right as the first character renders will see the full prompt billed and roughly 5–30 output tokens — pennies for almost any production workload. The cost angle isn't the thing to lose sleep over. The connection-pool angle is. More on that below.

Node.js (TypeScript): wire up AbortController properly

The Anthropic SDK's messages.stream() accepts an AbortSignal. The cleanest pattern in Express is to bind a controller to the request lifecycle so a client disconnect tears the upstream stream down too.

import Anthropic from "@anthropic-ai/sdk";
import express from "express";
 
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY! });
const app = express();
 
app.post("/api/chat/stream", express.json(), async (req, res) => {
  // Tear down the upstream call when the browser disconnects
  const controller = new AbortController();
  req.on("close", () => controller.abort());
 
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache, no-transform");
  res.setHeader("Connection", "keep-alive");
 
  let outputTokens = 0;
  try {
    const stream = await client.messages.stream(
      {
        model: "claude-sonnet-4-6",
        max_tokens: 1024,
        messages: req.body.messages,
      },
      { signal: controller.signal },
    );
 
    for await (const event of stream) {
      if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
        res.write(`data: ${JSON.stringify({ text: event.delta.text })}\n\n`);
      } else if (event.type === "message_delta") {
        // Keep overwriting — the last value before abort is what you pay for
        outputTokens = event.usage.output_tokens;
      }
    }
 
    res.write(`data: [DONE]\n\n`);
    res.end();
  } catch (err: any) {
    // AbortError is the success path for "user clicked Stop"
    if (err.name === "AbortError" || controller.signal.aborted) {
      console.log(`Stream cancelled. Output tokens billed: ${outputTokens}`);
      return; // res is already closed
    }
    console.error(err);
    res.write(`data: ${JSON.stringify({ error: "stream_failed" })}\n\n`);
    res.end();
  } finally {
    await recordUsage({ outputTokens, cancelled: controller.signal.aborted });
  }
});
 
async function recordUsage(_: { outputTokens: number; cancelled: boolean }) {
  // Persist to your metrics pipeline (Postgres, Datadog, whatever)
}

Three details matter here. First, req.on("close", () => controller.abort()) — without this line the upstream stream keeps consuming tokens after the user has closed the tab. Second, capture event.usage.output_tokens on every message_delta; the last value you saw before the abort is the one you'll be billed for. Third, treat AbortError as a success path, not an error. Logging it at error level is the fastest way to make your alerting useless.

✦

Thank you for reading this far.

Continue Reading

What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.

WHAT YOU'LL LEARN
✦Real drift numbers from 100 measured cancellations (12 mean, 27 p95, 34 max output tokens) so you know exactly what a Stop button costs
✦The 64x gap between drift and a missing close handler: roughly $0.29 vs $18.70 a month, and the mechanics behind it
✦A production ledger that composes user cancel, timeout, and SIGTERM into one AbortSignal while still recovering which one fired
Secure payment via Stripe · Cancel anytime
✦

Unlock This Article

Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.

or
Unlock all articles with Membership →
Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

⬡ API & SDK2026-09-07
A 75% cheaper cache read moved one bill by 29% and another by 2.8%
Fable 5.1 and Mythos 5.1 changed the cache read multiplier from 0.1 to 0.025. Here is how to estimate your own saving from your token mix, and how to fix code that hard-codes the multiplier.
⬡ API & SDK2026-06-12
Handing Monthly Revenue CSVs to the Claude API Code Execution Tool — Files API Wiring, Container Reuse, and Billing Traps
How I moved monthly revenue-CSV reconciliation into the Claude API Code Execution tool — a pre-upload sanity check, Files API integration, container reuse, the 5-minute billing minimum, and the file recovery that pause_turn quietly drops.
⬡ API & SDK2026-05-25
A Two-Tier Setup — Haiku 4.5 Orchestrator with Opus 4.6 Worker for Balancing Cost and Quality
An indie developer's two-tier setup — Haiku 4.5 as orchestrator, Opus 4.6 as worker — costed line by line against published rates, plus the price increase I planned around that never arrived and the tokenizer split that quietly moves every token-denominated threshold.