# Why Streaming Changes How You Build LLM-Powered Interfaces

When I started building on the Anthropic API, the first thing I had to stop treating as a detail was streaming. It's easy to prototype an LLM feature with a standard request-response cycle — send a message, wait, render the result. It works fine until a real user is on the other end staring at a blank screen for three seconds.

Streaming is the difference between a chatbot that feels alive and one that feels like a form submission.

## What Streaming Actually Is

Streaming generates and delivers text incrementally as the model produces it, rather than waiting for the full response to be ready. The transport mechanism is Server-Sent Events (SSE) — a one-way channel where the server pushes data to the client as it's available.

From a product perspective, this matters for two reasons:

**Perceived latency drops dramatically.** Users see the first token in under a second instead of waiting for the full response. Even if the total generation time is the same, the experience feels faster because something is happening immediately.

**You can cancel early.** If the model starts going in the wrong direction, the user (or your system) can cut the stream and save tokens. In agentic workflows where model calls chain together, this adds up.

When should you *not* use streaming? Backend pipelines where a machine is consuming the output, data extraction tasks where you need the complete structured response before you can do anything, and automated agent workflows where partial state is worse than no state. Streaming is a mechanism for incrementally delivering content — it's not always the right one.

## The Bridge Problem

Here's where it gets interesting at the implementation level. The Anthropic SDK exposes streaming as an async iterator — it gives you chunks as they arrive through a `for await` loop. That's great for server-side code. But a `Response` object in a Next.js API route expects a `ReadableStream`, not an async iterator. They're not the same thing.

The solution is a `ReadableStream` wrapper that bridges the two:

```typescript
const readable = new ReadableStream({
  async start(controller) {
    for await (const chunk of stream) {
      if (
        chunk.type === 'content_block_delta' &&
        chunk.delta.type === 'text_delta'
      ) {
        controller.enqueue(chunk.delta.text);
      }
    }
    controller.close();
  },
});
```

What this does: `ReadableStream` maintains an internal queue. As chunks arrive from the Anthropic stream, we filter for the ones we care about (`content_block_delta` events with `text_delta` type) and push them into that queue via `controller.enqueue()`. The client reads from the queue via `reader.read()`, which suspends when the queue is empty and resumes when a new chunk arrives. When `controller.close()` is called, `read()` returns `done: true` on the next call.

It's a producer/consumer pattern — the Anthropic stream is the fast producer, the client is the slower consumer, and `ReadableStream` is the buffer between them.

## Understanding the Stream Structure

Every Anthropic streaming response follows the same envelope pattern:

```plaintext
message_start
  content_block_start
    content_block_delta
    content_block_delta
    ...
  content_block_stop
  content_block_start
    content_block_delta
    content_block_delta
  content_block_stop
message_stop
```

`message_start` carries the outer metadata — model, token usage, stop reason. The actual content lives in `content_block_delta` events. For basic text responses, you only need to track `content_block_delta` with `type: 'text_delta'`. Once you add tool use to the mix (covered in the next post), `content_block_start` becomes critical because it tells you what *type* of block is opening — text or a tool call.

## Why This Matters for Production Systems

The `ReadableStream` wrapping pattern isn't just a TypeScript quirk — it's a concrete example of a problem that comes up constantly in applied AI work: the model's output format doesn't match what your application layer expects, and you need an adapter layer between them.

That adapter layer — whether it's a stream bridge, a JSON parser, or a response transformer — is often where the real engineering work happens in LLM-powered products. The model call itself is straightforward. Making its output integrate cleanly with the rest of your stack is not.
