Skip to content
bits
Ship an agent to the edge · Lesson 2 of 8

Stream the reply

Tutor:

Last lesson's Worker waits for Claude's whole answer, then sends it. For a one-line reply that's fine. For a long answer, or an agent that calls three tools first, the user stares at a spinner the whole time.

Streaming sends each piece of text the moment Claude writes it. Same answer, same cost, but the first words arrive almost at once.

Buffered vs Streamedillustrative: made-up numbers that show the idea, not output from a real modelcompare

promptAfter 1 second

Buffered

(waiting…)

Streamed

Chicago is overcast,

promptAfter 4 seconds

Buffered

(waiting…)

Streamed

Chicago is overcast, 41°F, with light wind from the north. Bring a jacket.

promptAfter 5 seconds

Buffered

Chicago is overcast, 41°F, with light wind from the north. Bring a jacket.

Streamed

(done)

Tutor:

The SDK's messages.stream() gives you events. You keep the text deltas and pass them into a ReadableStream, which becomes the response body:

import Anthropic from "@anthropic-ai/sdk";
export default {
async fetch(request, env): Promise<Response> {
const { question } = await request.json<{ question?: unknown }>();
if (typeof question !== "string" || question.length > 2000) {
return new Response("question must be a string under 2,000 characters", { status: 400 });
}
const client = new Anthropic({ apiKey: env.ANTHROPIC_API_KEY });
const stream = client.messages.stream({
model: "claude-haiku-5-5",
max_tokens: 1024,
output_config: { effort: "low" },
messages: [{ role: "user", content: question }],
});
const encoder = new TextEncoder();
const body = new ReadableStream({
async start(controller) {
try {
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();
} catch (error) {
controller.error(error);
}
},
cancel() {
stream.abort(); // the reader left: stop paying for tokens nobody reads
},
});
return new Response(body, { headers: { "content-type": "text/plain; charset=utf-8" } });
},
} satisfies ExportedHandler<Env>;
TypeScriptUTF-8● lines 10–34: stream the reply
OUTPUT
curl -N $URL -d @question.json
# example output: the words arrive a few at a time
Layer up: November in Chicago swings from mild to freezing,
and the wind off the lake makes it feel colder…
Tutor:

Two Workers facts make this safe. Waiting doesn't count as CPU time. The CPU limit (10 ms on the free plan, 30 seconds by default on paid, as of October 2026) only counts time your code is computing. Seconds spent waiting on Claude are free. And an HTTP request has no wall-clock limit while the client stays connected, so a long stream can run to the end.

The flip side: when the client disconnects, Cloudflare may cancel the work. That's what cancel() is for. If the reader closes the tab, abort the model request so you aren't billed for the rest of an answer nobody will see.

quick check+20 neurons
Tutor:

On the free plan (10 ms of CPU per request), your Worker waits 8 seconds for Claude to finish streaming. What happens?

    Type your answer or tap an option · Enter checks

    quick check+20 neurons
    Tutor:

    A user closes the tab halfway through a long answer. What should your Worker do?

      Type your answer or tap an option · Enter checks