C.W.K.
Stream
Lesson 01 of 04 · published

Why Streaming & SSE Format

~12 min · streaming, sse, wire-format

Level 0Spark
0 XP0/35 lessons0/10 achievements
0/140 XP to next level140 XP to go0% complete

Streaming buys you time-to-first-token

Time-to-first-token (TTFT) on Gemini 2.5 Flash is typically 200–400ms. Time-to-last-token on a 500-word reply is 3–8 seconds. Streaming doesn't make the model faster — it makes the user's wait visible progress instead of a blank screen.

The product cost of skipping streaming is the difference between "this app is slow" and "this app is alive." Same total time. Different perception.

Server-Sent Events — the wire format

Gemini's streaming endpoint speaks Server-Sent Events (SSE). Each event is a line that starts with data:, followed by a complete JSON object — the same shape as a non-streaming response, but with only a slice of text in the parts array.

Two endpoint variants

Same URL, different query string:

  • /streamGenerateContent (no ?alt=sse) — returns a buffered JSON array. Useful when you want streaming chunks but don't want to parse SSE.
  • /streamGenerateContent?alt=sse — returns SSE. Useful when you want to forward the stream to a browser, or you're parsing line-by-line.

The final chunk carries usageMetadata

Token counts only show up on the last chunk of the stream. If you want to bill or log the call, accumulate text across all chunks and grab usageMetadata off whichever chunk has it.

Code

Raw SSE wire — what comes off the socket·text
POST .../models/gemini-2.5-flash:streamGenerateContent?alt=sse
x-goog-api-key: $GEMINI_API_KEY
Content-Type: application/json

# Response headers:
Content-Type: text/event-stream

# Response body (each block separated by blank line):
data: {"candidates":[{"content":{"parts":[{"text":"Hello"}],"role":"model"}}]}

data: {"candidates":[{"content":{"parts":[{"text":" world"}],"role":"model"}}]}

data: {"candidates":[{"content":{"parts":[{"text":"!"}],"role":"model"},"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":4,"candidatesTokenCount":3,"totalTokenCount":7}}
Chunk shape — same envelope, partial text·json
{
  "candidates": [{
    "content": {
      "parts": [{"text": "hello"}],
      "role": "model"
    },
    "finishReason": "STOP"
  }],
  "usageMetadata": {
    "promptTokenCount": 10,
    "candidatesTokenCount": 5,
    "totalTokenCount": 15
  }
}
Curl the stream by hand·bash
curl -N \
  "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse" \
  -H "x-goog-api-key: $GEMINI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"contents":[{"parts":[{"text":"Tell me a haiku about coffee."}]}]}'

# -N disables curl's output buffering so you actually see chunks as they arrive.

External links

Exercise

Run the curl command above with -N, then run it again without -N. Notice the difference in how chunks arrive. Then redirect the output to tee stream.log and read the log to confirm the final event has usageMetadata while earlier ones don't. Write one sentence on why ?alt=sse + -N is the canonical curl recipe for stream debugging.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.