Claude for Developers

Lesson 8 of 14

Streaming, stop reasons, and how responses end

Two facts force streaming on any serious application. Users abandon interfaces that sit silent for twenty seconds while a long answer generates. And long generations over plain HTTP eventually hit connection timeouts — which is why the SDKs require streaming for large max_tokens. Streaming solves both, and along the way this lesson covers the field you must read on every response, streamed or not: stop_reason.

Streaming with the SDK helper

The raw wire format is server-sent events, but the SDK helper hides the event bookkeeping and gives you the two things you actually want — text as it arrives, and the complete message at the end:

python
with client.messages.stream(
    model="claude-opus-5",
    max_tokens=64000,
    messages=[{"role": "user", "content": "Write a detailed migration guide from REST to gRPC."}],
) as stream:
    for text in stream.text_stream:
        print(text, end="", flush=True)          # tokens as they arrive

    final = stream.get_final_message()            # the assembled Message object
    print(final.usage.output_tokens, final.stop_reason)

That get_final_message() call is the pattern to internalise: stream for delivery, then work with the final message for logic. Your UI consumes the token stream; your history-appending, tool-loop, and logging code operates on the same complete Message you would have had without streaming. Never hand-assemble the final message from deltas — the helper exists so you don't.

The habit worth adopting even for short calls: streaming plus get_final_message() gives you timeout protection for free, because data flows continuously instead of one long silent wait. Default to it; opt out only for quick, small calls.

Stop reasons: the response's exit code

Every response carries stop_reasonwhy generation ended — and production code branches on it the way shell scripts branch on exit codes. The values you will actually meet:

  • end_turn — the model finished naturally. The happy path.
  • max_tokens — generation hit your cap and the output is truncated mid-thought. Treating this as success is how half-written JSON reaches a parser. Detect it; raise the cap or shorten the task.
  • tool_use — the model is requesting tool calls; the loop from the tools lesson takes over.
  • pause_turn — a long server-tool turn paused; re-send the conversation as-is and the server resumes.
  • stop_sequence — a custom stop string you configured was hit.
  • refusal — safety systems declined to continue. The content may be empty or partial, which is why code that unconditionally reads content[0] breaks here. Surface the situation honestly to the user; don't blind-retry the identical request.
  • model_context_window_exceeded — the input filled the window before output could finish; trim or summarize history (the agents lesson shows the durable fixes).

The rule that unifies them: check stop_reason before consuming content. It is one line, and it is the difference between a system that degrades honestly and one that silently ships truncated or empty output downstream.

python
final = stream.get_final_message()
if final.stop_reason == "refusal":
    return explain_refusal(final)
if final.stop_reason == "max_tokens":
    log.warning("truncated response", request_id=final._request_id)
text = "".join(b.text for b in final.content if b.type == "text")

Sizing max_tokens now that you stream

Streaming removes the timeout reason to keep max_tokens small, so give the model room: 64,000 is a sensible streaming default, and the frontier models go to 128,000 when you need long documents. Remember from the earlier lessons that thinking spends from the same budget — a deep-reasoning request with a tight cap produces a long think and a truncated answer, which reads as a model failure but is a configuration failure.

What to take into the next lesson

Stream for users and for timeout safety; get_final_message() for logic; branch on stop_reason always; size the cap generously. With input economics (caching) and output delivery (streaming) in place, the next lessons move up a level — from single calls to systems: grounding Claude in your data, and then agents that run loops on their own.

← Previous