Claude API Streaming Response in Golang: A Guide
Streaming responses from the Claude API in Go means consuming a server-sent events (SSE) stream over HTTP and parsing each data: line as it arrives, rather than waiting for the full response body. Go doesn't have a built-in SSE client, so you typically read the response body with bufio.Scanner or bufio.Reader, split on newlines, and decode each JSON chunk as Claude sends it.
This matters because without streaming, your Go service blocks until Claude finishes generating the entire response — which can take 10-30 seconds for longer completions. Streaming lets you forward tokens to a frontend, a CLI, or a websocket connection in near real-time, which is essential for any chat-like UI or long-form generation task.
How Claude's streaming format works
Claude's API streams responses as SSE events. Each event has a type field such as message_start, content_block_delta, message_delta, and message_stop. The actual text tokens arrive inside content_block_delta events, nested under delta.text.
A raw stream looks like this:
event: message_start
data: {"type":"message_start","message":{"id":"msg_123","model":"claude-3-5-sonnet-20241022","usage":{"input_tokens":10}}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":" world"}}
event: message_stop
data: {"type":"message_stop"}
Your Go code needs to parse each data: line, ignore the event: lines if you don't need them, and accumulate or forward the delta.text fragments.
Setting up the HTTP request
You need "stream": true in your request body and the response must be read incrementally, not buffered with io.ReadAll.
package main
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"net/http"
"strings"
)
type deltaEvent struct {
Type string `json:"type"`
Delta struct {
Text string `json:"text"`
} `json:"delta"`
}
func streamClaude(apiKey, prompt string) error {
body := map[string]any{
"model": "claude-3-5-sonnet-20241022",
"max_tokens": 1024,
"stream": true,
"messages": []map[string]string{
{"role": "user", "content": prompt},
},
}
payload, _ := json.Marshal(body)
req, err := http.NewRequest("POST", "https://api.anthropic.com/v1/messages", bytes.NewReader(payload))
if err != nil {
return err
}
req.Header.Set("x-api-key", apiKey)
req.Header.Set("anthropic-version", "2023-06-01")
req.Header.Set("content-type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
// Default scanner buffer may be too small for large lines
buf := make([]byte, 0, 64*1024)
scanner.Buffer(buf, 1024*1024)
for scanner.Scan() {
line := scanner.Text()
if !strings.HasPrefix(line, "data: ") {
continue
}
raw := strings.TrimPrefix(line, "data: ")
var evt deltaEvent
if err := json.Unmarshal([]byte(raw), &evt); err != nil {
continue
}
if evt.Type == "content_block_delta" {
fmt.Print(evt.Delta.Text)
}
}
return scanner.Err()
}
A few things to watch out for:
- Buffer size: the default
bufio.Scannerbuffer (64KB) can silently truncate long lines. Always callscanner.Buffer()with a larger size. - Keep-alive blank lines: SSE streams include blank lines between events. Your scanner loop should just skip anything that doesn't start with
data:. - Context cancellation: wrap the request with
context.WithTimeoutorcontext.WithCancelso a client disconnect propagates upstream and you're not holding an open connection to Claude indefinitely.
Forwarding the stream to your own clients
If you're building a Go backend that proxies Claude's stream to a browser or mobile client, the common pattern is to re-emit SSE events on your own HTTP response using http.Flusher:
func handleChat(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
return
}
// ... inside your token loop:
fmt.Fprintf(w, "data: %s\n\n", token)
flusher.Flush()
}
This gives you a clean separation: your Go service talks to Claude over one SSE connection and re-streams tokens to the browser over a second SSE connection, which is the standard pattern for chat UIs built with Go backends.
Handling errors mid-stream
Streams can fail partway through — a dropped connection, a rate limit hit after the first few tokens, or a malformed JSON chunk. Always check scanner.Err() after the loop ends, and consider tracking whether you received a message_stop event before the stream closed. If you didn't, treat it as an incomplete response and retry with backoff rather than assuming the generation finished cleanly.
Skipping the plumbing with SubToAPI
If you're maintaining this SSE parsing, buffer management, and retry logic across several Go services, it adds up. SubToAPI turns your Claude access into a standard HTTPS API with application keys (sub_live_...), so your Go code hits one consistent streaming endpoint with the same request/response shape regardless of which underlying model or account is used. You get usage metadata per request and team seats for managing multiple services, without rewriting your scanner logic for every client. Check the streaming docs and quickstart to see the request format, or start a trial at signup.
questions
Does the Claude API support streaming natively, or do I need a special client for Go? Claude's API streams via standard SSE over HTTPS. Go has no built-in SSE client, but bufio.Scanner combined with a larger buffer size is enough to parse the data: lines manually — no third-party library required.
Why does my Go stream appear to cut off early? This is usually a scanner buffer limit. The default 64KB buffer can truncate long lines. Call scanner.Buffer() with a bigger capacity, and verify you're checking for the message_stop event rather than just closing on EOF.
Can I use goroutines to process streamed tokens concurrently? You can forward tokens to a channel and process them in a separate goroutine, but keep the scanning loop itself single-threaded since SSE lines must be read in order. Use a buffered channel so slow consumers don't block the read loop.