SwiftRelay

← All sprint notes

Green Light Checklist for SSE Streaming Responses

Protocol pit stopEN

Does the Client Accept a Text Event Stream?

Streaming with server-sent events starts with a yes/no decision you make before the first request goes out: does your client accept a text event stream as the response body? If the answer is no, the payload will sit in a buffer until the connection closes, and the reason for streaming is gone.

SSE rides on an ordinary HTTP response. The server sets the content type to text/event-stream, keeps the connection open, and writes events as they are produced. Your client reads the body incrementally instead of waiting for one final document. There is no WebSocket handshake, no polling loop, and no second protocol to manage.

Say yes when the surface is interactive: a chat window, a CLI that prints tokens as they arrive, or a progress panel for a long tool run. Say no when the consumer wants one complete parseable payload, such as a batch job writing results to a file or a webhook receiver that stores a single JSON record. A non-streaming request is the right answer more often than people admit.

Also check the middle layer. Proxies and serverless runtimes that collect the full response before forwarding will silently turn your stream into a buffered download. If you cannot answer yes for every hop between the model and the client, fix that before writing any parsing code.

Do You Need Token-by-Token Events?

Second question: do you need events as they are produced, or is one complete answer acceptable? This is the gap between progressive rendering and a finished payload, and it decides how much state your client has to carry.

Most assistant surfaces want incremental output. Claude Code, Codex and Gemini CLI print text as it arrives because a blank screen after the request feels wrong once a user has watched an answer build line by line.

Incremental output also matters for structured work. When a model emits tool calls, the arguments usually arrive as partial JSON spread across several events. Buffer until the end and you lose any chance to show progress, but execute a tool on every fragment and you will run it against an incomplete payload. The practical rule is simple: render fragments, execute only after the stream signals completion.

  • The answer can run long enough that a user starts reading before it ends.
  • The first lines are useful before the last line exists.
  • You want the option to cancel mid-answer without discarding the visible work.
  • Tool calls or long plans need a visible in-progress state.

If none of those apply, ask for a complete response instead. It is simpler to test, simpler to retry, and simpler to store. A yes here is only worth the extra state when someone actually watches the output arrive.

Is the Parser Ready for Real Event Boundaries?

Third question: does your parser handle the wire format exactly? SSE looks trivial and then breaks on the details, so answer yes only once every case below is covered.

The stream is a sequence of events separated by blank lines. Each event is built from field lines: data carries the payload, event names the type, and id gives the event an identifier. A line that starts with a single colon is a comment, commonly used as a keep-alive. Multiple data lines inside one event are joined with a newline before dispatch.

  • Split on newlines that arrive in arbitrary chunks, not on individual socket reads. One read can hold half an event or three whole ones.
  • Join consecutive data lines and dispatch only when the blank line arrives.
  • Treat an unknown event name as ignorable rather than fatal.
  • Watch for the end-of-answer sentinel that OpenAI-compatible streams send after the last chunk.
  • Keep an accumulator per content block when the text is assembled from deltas.

Anthropic-style streams send named events such as a message start, content deltas, and a final stop event. OpenAI-compatible streams send data chunks carrying a choices array with delta text. Gemini streams similar incremental payloads through its own streaming method. All three fit the same reader if you branch on the payload shape after the event is parsed.

Test the parser against a recorded fixture before you point it at a live model. Feed it a stream split at awkward boundaries and assert that the reassembled text matches the original answer character for character.

Can the App Survive a Mid-Stream Failure?

Fourth question: can your app handle a failure that arrives after the first event? The status code tells you almost nothing once the headers are out, so the answer has to come from the stream body itself.

Decide what an interrupted answer leaves behind. Partial text is usually displayable but never safe to persist as final. Tool calls built from incomplete argument fragments must be discarded. Keep a flag that marks output as provisional until the terminal event arrives, and let the storage layer act on that flag.

  • Surface a distinct error state instead of stopping silently at the last good fragment.
  • Cancel through the client abort API when the user navigates away, so the connection closes cleanly.
  • Decide your retry policy before you build. Replaying a long stream duplicates work the user already watched.
  • Set idle timeouts that tolerate gaps between events, and treat prolonged silence as a warning, not as an answer.

Backpressure is the quiet failure. If the renderer is slower than the producer, buffers grow and the interface feels stuck even though events keep arriving. Push events through a queue that drops nothing but renders in order, and flush whatever remains when the terminal event lands.

Finally, decide whether you need resumption. Some protocols support replay from an event identifier, but most model streams are one-shot. Design for a clean restart instead of assuming a dead connection can be stitched back together.

Does the Base URL Match the Client?

Fifth question: does the base URL match what the client expects? This is the check that turns a working stream into a 404 before a single event is written.

Claude Code and Gemini CLI take the bare host: https://api.fastapirelay.com. OpenAI SDKs point at the same host and add /v1 for the API prefix. Get this wrong and the request never reaches a streaming endpoint at all, which looks exactly like a broken parser.

Then confirm the model name and the streaming flag in the same breath. A single smoke run with Claude Haiku 4.5 answers the questions above at once: check the response headers, watch where events begin and end, and confirm that the terminal marker actually arrives.

SwiftRelay is a pay-as-you-go relay for Claude, GPT, Gemini, DeepSeek, Kimi, GLM, Qwen, Grok and other models, billed per token, and it works with Claude Code, Codex, Gemini CLI and any OpenAI-compatible SDK. New accounts receive a small gift credit, and inviting friends earns 14% of their top-ups. If every item on this checklist is a yes, sign up for SwiftRelay and run your first streamed answer against the endpoint.

Re-run the five questions whenever you add a client, swap a model, or move the service behind another proxy. The answers stay stable per integration, and a short checklist beats an evening spent staring at an empty stream.

Keep reading

Back for another lap?Log in