On this page
Lesson 3 of 8
Streaming
What you'll learn
- Stream a response token-by-token to the user
- Handle backpressure and partial content cleanly
- Stop a stream safely on user cancel
Streaming feels good and saves real time. Users see the first token in under a second instead of waiting several seconds for a complete response. We will wire it end-to-end and cover the failure modes.
Why stream?
Without streaming, a call to Claude blocks until the entire response is generated. For a 500-token response from Sonnet 4.6, that might be 3-5 seconds of blank screen. With streaming, the first token arrives in a few hundred milliseconds. The total time is the same, but the perceived latency drops dramatically.
Stream when your output goes to a human. Skip streaming when you just need the final result for downstream processing.
Basic streaming with the SDK
The SDK provides a .stream() method that returns an async iterable:
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Explain closures in TypeScript." }],
});
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
process.stdout.write(event.delta.text);
}
}
// After the stream ends, get the full message object
const finalMessage = await stream.finalMessage();
console.log("\n\nStop reason:", finalMessage.stop_reason);
console.log("Usage:", finalMessage.usage);
The stream object emits Server-Sent Events. The key event types you will handle:
| Event type | When it fires | What it carries |
|---|---|---|
message_start | Once at the beginning | The message shell (id, model, role) |
content_block_start | Start of each content block | Block type and index |
content_block_delta | Each chunk of content | The text delta or tool use delta |
content_block_stop | End of a content block | Block index |
message_delta | Near the end | Stop reason, output token count |
message_stop | Final event | Empty — signals stream is done |
Collecting the full text
If you need both streaming display and the complete text, use the helper:
const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Write a haiku about APIs." }],
});
stream.on("text", (text) => {
process.stdout.write(text); // display incrementally
});
const message = await stream.finalMessage();
const fullText =
message.content[0].type === "text" ? message.content[0].text : "";
// fullText has the complete response
The .on("text", ...) helper filters for text deltas so you do not need to check event types manually. The finalMessage() call waits for the stream to complete and returns the same response shape you get from a non-streaming call.
Cancellation with AbortController
When a user clicks "Stop generating," you need to cancel the request server-side so you stop paying for tokens Claude generates after the cancel. Use AbortController:
const controller = new AbortController();
const stream = client.messages.stream(
{
model: "claude-sonnet-4-6",
max_tokens: 2048,
messages: [{ role: "user", content: longQuestion }],
},
{ signal: controller.signal }
);
// Somewhere else — user clicks Stop
setTimeout(() => {
controller.abort();
console.log("Stream cancelled by user.");
}, 5000);
try {
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
process.stdout.write(event.delta.text);
}
}
} catch (error) {
if (error instanceof Error && error.name === "AbortError") {
console.log("\nGeneration stopped.");
} else {
throw error;
}
}
When you abort, the SDK closes the connection and throws an AbortError. Catch it explicitly — it is not a bug, it is the user's intent. You still keep whatever text arrived before the abort.
Backpressure
Backpressure occurs when your consumer (a database write, a network forward to the browser) is slower than Claude's token generation. The for await loop handles this naturally — it only pulls the next event when the current iteration completes. If you write tokens to a slow stream, the loop pauses automatically.
For server-to-browser forwarding in a web framework, pipe the tokens through a ReadableStream:
function streamToClient(userMessage: string): ReadableStream {
const encoder = new TextEncoder();
return new ReadableStream({
async start(controller) {
const stream = client.messages.stream({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: userMessage }],
});
for await (const event of stream) {
if (
event.type === "content_block_delta" &&
event.delta.type === "text_delta"
) {
controller.enqueue(encoder.encode(event.delta.text));
}
}
controller.close();
},
});
}
Streaming with tool use
When Claude decides to use a tool during streaming, you will see content_block_start with type: "tool_use", followed by input_json_delta events that deliver the tool's input arguments incrementally. You typically buffer these deltas, parse the complete JSON when content_block_stop fires, execute the tool, and continue the conversation. We will cover this in detail in the next lesson.
What's next
You can now deliver responses in real time. The next lesson — Tool use — teaches Claude to call your functions. You will learn the tool definition schema, the tool_use/tool_result message loop, and how to handle errors when tools fail.
التّدفّق ممتع ويوفّر وقتًا حقيقيًّا. المستخدمون يرون أوّل رمز في أقلّ من ثانية بدل انتظار عدّة ثوانٍ لاستجابة كاملة. سنربطه من الطّرف للطّرف ونغطّي أنماط الإخفاق.
لماذا نُدفّق؟
بدون تدفّق، يحجب استدعاء Claude حتّى تُولَّد الاستجابة كاملة. مع التّدفّق، يصل أوّل رمز في بضع مئات من الميلّيثوان. الزّمن الكلّي نفسه، لكنّ الكمون المحسوس ينخفض بشكل كبير. دفّق حين يذهب المخرج لإنسان. تخطّ التّدفّق حين تحتاج النّتيجة النّهائيّة فقط لمعالجة لاحقة.
التّدفّق الأساسي مع SDK
SDK يوفّر طريقة .stream() تُرجع مُتكرّرًا غير متزامن. أنواع الأحداث الأساسيّة: message_start (بداية الرّسالة)، content_block_delta (كلّ جزء من المحتوى يحمل نصًّا تدريجيًّا)، message_delta (سبب التّوقّف قرب النّهاية)، و message_stop (انتهاء التّدفّق).
جمع النّصّ الكامل
إذا كنت تريد العرض التّدفّقي والنّصّ الكامل معًا، استعمل مساعد .on("text", ...) للعرض التّدريجي، ثمّ finalMessage() للحصول على نفس شكل الاستجابة من الاستدعاء غير المتدفّق.
الإلغاء بـ AbortController
حين ينقر المستخدم "أوقف التّوليد"، تحتاج إلغاء الطّلب من جهة الخادم حتّى تتوقّف عن الدّفع مقابل رموز يولّدها Claude بعد الإلغاء. استعمل AbortController ومرّر signal إلى خيارات الاستدعاء. حين تُلغي، SDK يغلق الاتّصال ويرمي AbortError — التقطه صراحةً.
الضّغط الخلفي
يحدث حين يكون مستهلكك أبطأ من توليد رموز Claude. حلقة for await تعالج هذا طبيعيًّا — تسحب الحدث التّالي فقط حين تكتمل التّكرارة الحاليّة. لتوجيه الرّموز من الخادم إلى المتصفّح، استعمل ReadableStream.
التّدفّق مع استعمال الأدوات
حين يقرّر Claude استعمال أداة أثناء التّدفّق، سترى أحداث input_json_delta تنقل وسائط الأداة تدريجيًّا. عادةً تخزّنها مؤقّتًا، تحلّل JSON الكامل عند content_block_stop، تنفّذ الأداة، وتتابع المحادثة. سنغطّي هذا بالتّفصيل في الدّرس القادم.
ما التّالي
يمكنك الآن تقديم الاستجابات في الوقت الحقيقي. الدّرس القادم — استعمال الأدوات — يعلّم Claude استدعاء دوالّك. ستتعلّم مخطّط تعريف الأداة وحلقة tool_use/tool_result وكيفيّة معالجة الأخطاء حين تفشل الأدوات.
Try it yourself
Build a small chat UI that streams responses. Add a Stop button that actually stops generation server-side.
Reflect
In what situations would you choose non-streaming over streaming? When does the simpler API win?