- The build-or-buy question is really about the streaming contract: the guarantees between the model's event stream and what a user sees on screen.
- Nobody adopts an SDK to avoid writing a fetch loop. The happy path is about fifty lines. The SDK is selling you the failure list.
- Server-sent events won. Anthropic, OpenAI, MCP and AG-UI all push typed events over plain HTTP, and browsers reconnect for free with
Last-Event-ID. That is also the cheapest decision on this page. - Deltas and snapshots are opposites. One you append, one you reconcile. Getting it wrong is invisible for text and catastrophic for tool rows, charts and cards.
- A turn reaches the screen by four routes, not one: fresh send, resume, reconnect, and resume after approval. Every rendering rule gets written once per route unless you plan otherwise.
- Build your own only if you fail one of three tests: transport, durability, or event vocabulary. Zero failures means use the SDK.
- The option most teams skip: keep the SDK's client state machine and replace only the transport. That covers durable runs without a rewrite.
- Measure time to first meaningful event and resume success rate. Tokens per second is a model metric and rarely your bottleneck.
Part 1 · The decision
The choice is not about a library
You are not picking a dependency. You are picking who gets paged when a stream stalls.
The first version always works. You call streamText on the server, drop useChat into a React component, and within an afternoon text is crawling across the screen the way it does in every product demo you have ever seen. The whole thing fits on one screen of code.
Two months later the bug reports start, and none of them are about the model. A user refreshes at the fourteen second mark and the half-written answer vanishes for good. One enterprise customer, sitting behind a reverse proxy nobody on your team configured, gets the entire response delivered in a single lump after thirty seconds of blank panel. A tool call renders as {"cit for four hundred milliseconds, and somebody screenshots it into a Slack channel with a laughing emoji. Your stop button stops the UI while the server keeps burning tokens on a request nobody is reading.
Those failures share an address. They all live in the space between the model emitting events and your interface painting pixels.
What is the streaming contract?
An honest answer to the build-or-buy question needs both columns of the ledger, so start with what one turn actually costs.
Part 2 · Why it matters
Streaming stopped being a text effect
Agent UIs no longer stream a string. They stream a state machine.
Jakob Nielsen fixed the numbers decades ago and nothing about human perception has moved since. Under 0.1 seconds an interface feels like direct manipulation. Around one second a user keeps their train of thought but knows the machine is working. Past ten seconds attention drifts and people start doing something else.
A single agent turn with two tool calls lands somewhere between twenty and ninety seconds. Every batched response blows through the attention limit by a factor of three or more. Streaming does not make the agent faster by a single millisecond. It moves the user from wondering whether the thing crashed to watching it work, and that difference decides whether they wait.
The bigger shift is what travels down the pipe now. Claude's Messages API opens with message_start, then wraps each block in content_block_start, a run of content_block_delta events and a content_block_stop, closing with message_delta and message_stop. Those deltas are not all text: input_json_delta carries tool arguments assembling character by character, thinking_delta carries reasoning, citations_delta carries sources. Keep-alive pings turn up anywhere. The AG-UI protocol formalises the same idea across vendors with roughly sixteen event types spanning text, tool lifecycle, state patches and agent handoffs.
Add the things a real assistant surface has, a tool call that appears as it runs, a chart drawn from a query result, a card when a booking succeeds, an approval prompt that pauses the turn, and the two dozen tutorial lines stop being enough. Not because streaming text is hard. Because a turn is no longer a string, and a string is the only thing that survives being delivered twice without anyone noticing.
Your interface is no longer appending characters to a paragraph. It is reducing a typed event stream into a live view of what an agent is doing, and it has to stay honest at every intermediate frame.
Eerly AI StudioPart 3 · What you are buying
What an SDK is actually selling you
Not the fetch loop. A list of things that already went wrong for somebody else.
Engineers reject streaming SDKs for a reasonable-sounding reason: the code they replace is short. Parsing text/event-stream takes twenty lines. Appending deltas to React state takes ten more. If that were the job, rolling your own would be obvious.
The job is the other ninety percent. Modern chat SDKs ship a typed split between the message you store and the message you send the model, tool call accumulation that reassembles fragmented JSON, explicit status states for submitted, streaming, ready and error, a stop control, regeneration, provider normalization so Anthropic and OpenAI and Gemini events reduce to one shape, and a resume option that reconnects to a stream still running on the server. AI SDK 5 dropped its custom wire format for plain SSE in mid-2025 precisely so this layer could be debugged with curl.
None of that is clever. All of it is scar tissue.
Part 4 · The failure list
The eleven things that break
Every item here has a public bug thread attached to it. None of them involve the model.
Proxies rewrite your stream. Nginx buffers proxied responses by default and re-chunks them on its own schedule, which splits markdown across boundaries so **bold** arrives as three fragments and renders as literal asterisks. The fix is a header (X-Accel-Buffering: no) and a cache directive, and the debugging session that finds it usually takes a week because the bug only reproduces in one customer's environment.
Open streams eat connection slots. Browsers allow six concurrent HTTP/1.1 connections per origin. An SSE response never completes, so each open stream holds a slot until it closes. Six tabs and the rest of the app stops loading. HTTP/2 multiplexing removes the ceiling, which is a deployment decision your frontend team does not control.
A refresh is not a cancel. This one is subtle enough that the AI SDK documentation calls it out directly: if you pass the request abort signal through to the model call, a page unload kills the generation you were relying on resumption to keep alive. Disconnects and explicit stops need separate paths, and a stop needs a real server-side endpoint that clears the active stream.
Tool arguments are not JSON yet. Fragments arrive as partial JSON and only become parseable when the block closes. Render them raw and users watch {"query": "quarterly rev appear on screen. Buffer them entirely and you lose the fastest signal you have about what the agent is doing.
The remaining seven are in the table below. Read the last column first.
Part 5 · The repeat
The stream says it twice
A turn does not arrive once. It arrives by several routes, and one of them re-delivers everything.
Two event shapes travel down the same connection and look identical in a switch statement. A delta carries only what changed: a token, a tool call opening, a result landing. Append it and move on. A snapshot carries the entire conversation so far, every time, including messages you painted ten minutes ago. Reconcile it against what you have already rendered, or render all of it again.
Text survives the confusion by accident. Assign the same string to the same paragraph twice and nothing looks wrong, which is exactly why this bug stays hidden for months. It surfaces the day you render something that is not text: a tool row, a chart, a calendar card, an approval prompt.
Snapshots have no tense
Our own assistant surface shows a confirmation card when it books a meeting, built from the successful calendar tool result. It worked. Then cards started appearing on the wrong message, a meeting booked yesterday attached to today's "hi". The handler was not wrong about the data. It scanned the snapshot it was handed, found a successful booking, and had no way to know that result was ancient history.
The fix has two halves and both carry load. Bound the scan to messages after the last human message, which handles the live case. Seed a set of already-rendered card IDs from thread history, which handles the reload, where a freshly loaded thread makes every message look equally new. A dedupe set that starts empty on page load is not a dedupe set. It is one with a hole exactly where reloads happen.
A snapshot event has no tense. Everything inside it is happening now, as far as your handler can tell, and nothing in the payload marks which parts you have already seen.
Eerly EngineeringCount the routes to the screen
We thought we had a stream handler. We had four. Not by design: each arrived months apart carrying a genuinely different requirement, and each was written by someone reasonably concluding that the existing one did not quite fit.
Every rendering rule then got implemented four times and drifted four ways. How a tool row is built, when a card attaches, what counts as finished. Reconnect spent a long stretch showing tool activity without tool names, so reloading mid-run meant watching anonymous work happen.
The tempting response is a refactor ticket that unifies them. We tried, and stopped, because the diff was not a deduplication. Every difference between those handlers turned out to be a product question with a defensible answer on both sides. Should a reconnected stream re-announce tool names it already streamed once? Should an approval resume replay the reasoning that led to the prompt? Collapsing the four means answering all of that silently, inside a commit labelled refactor.
So the lesson is not that you should unify your handlers. It is that the number of routes a turn can take to the screen is a design input, not an implementation detail. Decide it before you write handler number one.
An ID is a promise about continuity
Once events repeat, everything you render needs an identity stable enough to survive being offered twice. Two failures taught us that, both cheap to avoid and expensive to find.
A chart could reach us two ways: from the tool message that produced it while streaming, and from the finished answer message, which carried a copy so a reload had something to read. Both routes were correct. Reading both rendered every chart twice. The fix was not a guard clause. We collapsed two extractors into one with a mode parameter, so the wrong call stopped being possible to make. An invariant held by a comment repeated at four call sites is a bug with a delay on it.
The second one was stranger. We seed a thinking row the moment a request goes out, so the wait has a shape, and the first real tool call takes its place. Removing the placeholder and adding the real row produced a visible flicker on every single turn. Consecutive tool rows group into one timeline, that group is keyed by the identity of its first row, and swapping the row changed the key, so the framework tore down and rebuilt the entire timeline instead of updating one line. Letting the real tool take over the placeholder in place fixed it. Group keys, animation state and scroll anchors all derive from identity, and reassigning one mid-stream remounts more than you expect.
Live and replayed are different products
While someone waits, every step is the value. Watching the agent search, read and write is the most useful thing on the screen. Reading that same thread back a week later, those steps bury the answer you came for. Same data, opposite job, so we render steps inline while live and collapse them behind a single expandable line on reload.
That sounds like a display toggle until you find the two cases that must not collapse: a thread the server still reports as running, and a turn paused awaiting approval. Fold away the step an approval card refers to and the card becomes unanswerable, since it asks someone to approve something whose description they now have to go digging for. Those two exceptions are the actual design work. The toggle was the easy half.
Part 6 · The rule
Three tests for building it yourself
Answer these before writing a line. Zero failures means the decision is already made.
Server-sent events won the transport argument. Anthropic and OpenAI stream typed events over HTTP, MCP and A2A adopted the same primitive, AG-UI rides on it, and EventSource reconnects on its own while resending a Last-Event-ID header the server can use to replay. Ops teams route it without a special case. Pick WebSockets when the client pushes heavily during a run, when you need binary frames, or when you want a control plane for cancellation running alongside an SSE data plane.
Transport settled, the real question is narrower. Run your project through three tests.
- The transport test. Does your environment forbid what the SDK assumes? A corporate proxy that only permits WebSockets, a Java or Python backend you cannot wrap in a Node route, no Redis anywhere in an air-gapped deployment, or several agents fanning out into one pane.
- The durability test. Must a run survive the browser? Hour-long agent executions, a user starting on desktop and checking on mobile, or a regulator who expects every intermediate step retained and replayable. Count the rows in Figure 5 for your product: each one is a route the contract has to hold on independently.
- The vocabulary test. Is the event stream itself product surface? Approval gates mid-run, provenance chips attached to individual claims, generative UI components the agent chooses, or shared state the user edits while the agent works.
Zero failures and the answer is the SDK, without a spike, without a bake-off. One failure and you replace one layer, usually the transport. Two or three and you own the protocol, in which case adopt an existing event vocabulary rather than inventing a private one nobody else can debug.
Part 7 · The middle path
The option most teams skip
Build or buy is a false pair. The layers unbundle.
Teams treat this as binary and then discover the seam. Modern chat SDKs expose transport as a replaceable class, which means you keep the typed message model, the status machine, the tool call accumulator and the rendering integration, and you swap only the piece that talks to your infrastructure. The AI SDK documents this shape directly: a transport with a reconnect hook, a persistence layer recording which stream is active for a conversation, a Redis-backed store holding the stream itself, and a GET endpoint that replays it as SSE on remount and returns 204 when nothing is running.
That is a few hundred lines against a stable interface. Compare it against a hand-written client, which is a few thousand lines that you now version, test across four browsers, and explain to whoever inherits it.
One layer up, a shared protocol solves a different problem. If your agents run in Python and your interface is React, an event vocabulary on the wire keeps the two from being welded together. AG-UI does this deliberately: an ordered stream of typed JSON events, transport-agnostic, with a middleware layer that tolerates loose matching so different backends can interoperate. Your frontend consumes events rather than a framework.
Three questions separate a streaming layer that survives production from a demo that does not: what happens when the connection drops, what happens when the user says stop, and what happens when two events disagree. Answer those first and the library choice mostly answers itself.
Eerly AI StudioAgent answers that arrive live, and arrive with receipts.
Eerly AI Studio streams agent work across the systems your teams already run, inherits the permissions configured inside them, and attaches provenance to every fact as it lands. No rip and replace, no second copy of your data.
Book a demo →Part 8 · The takeaway
Measure the contract, not the framework
Nobody outside your team can tell which library you chose. They can tell whether the stream holds.
Track four numbers and the argument gets settled by data. Time to first byte tells you the connection opened. Time to first meaningful event tells you when the user saw something other than a spinner, which is the only latency number that maps to how the product feels. The 95th percentile gap between events exposes buffering and stalls that averages hide completely. Resume success rate, the share of interrupted streams a client rejoins without losing content, tells you whether your durability story is real or aspirational.
Tokens per second belongs to the model vendor. It is rarely what makes an agent feel slow.
One caution about everything in Part 6, from a team that got this order wrong. The transport argument is the loudest one and the smallest one. Swap server-sent events for WebSockets, swap either for long polling, and every problem in Part 5 survives the change untouched: snapshots still carry no tense, four routes still lead to the same rendered turn, an ID you reassign mid-stream still remounts the timeline around it. Those bugs are consequences of one fact that no protocol fixes, which is that the stream will tell you something you already know and will not warn you first.
Pick the transport in an afternoon. Spend the week on the contract.
Use the SDK. Own the transport when your durability model demands it. Own the protocol when the event stream is your product. But whichever you choose, write down how a repeated turn renders once and how many routes reach the screen, because the version of this decision that goes badly is not the one made wrongly. It is the one nobody made on purpose.
Eerly AI StudioFAQ
Frequently Asked Questions
The questions engineering teams ask us most often about agent streaming, and the short answers.
Should you use an AI SDK or build your own streaming for an agent UI?
Use the SDK unless you cross one of three lines. The streaming happy path is roughly fifty lines of code, so the SDK is not saving you the fetch loop. It is saving you the failure list: proxy buffering, reconnection, abort semantics, partial tool arguments, markdown that flickers mid-token, and usage accounting. Build your own when your transport cannot be what the SDK assumes, when a run must survive the browser closing, or when the event vocabulary itself is your product surface. If none of those apply, a custom transport is a six-week detour to arrive where the SDK already is.
What is the streaming contract in an agent interface?
The streaming contract is the set of guarantees between the model's event stream and the pixels on screen: events arrive in order, partial state renders without lying, a disconnect does not destroy work in progress, cancellation actually cancels the generation on the server, and each rendered claim traces back to the event that produced it. Every agent UI has this contract whether or not the team wrote it down. The SDK question is really a question about who maintains it.
Should agent UIs use SSE or WebSockets?
Server-sent events are the default for agent UIs and the industry has converged there. Anthropic, OpenAI and the MCP and AG-UI specifications all stream typed events over HTTP, EventSource reconnects on its own and resends a Last-Event-ID header, and ops teams already know how to route plain HTTP. Reach for WebSockets when the client pushes heavily during a run, when you need binary frames, or when you want a separate control plane for cancellation and live user input alongside an SSE data plane.
What actually breaks when you roll your own LLM streaming?
The recurring failures are reverse proxies that buffer the response and re-chunk it so markdown splits across boundaries, the six connection per origin ceiling on HTTP/1.1 that open streams occupy, serverless timeouts that cut long agent runs, page refreshes treated as cancellations, partial tool arguments that are not valid JSON until the block closes, markdown parsers re-run on every token causing flicker, backpressure on slow clients, cumulative usage counters double counted per chunk, and unknown event types crashing the client instead of being ignored.
Can you keep the SDK and still control the transport?
Yes, and this is the option most teams should take. Modern chat SDKs expose the transport as a replaceable layer, so you keep the client state machine, the typed message model, the status handling and the tool call accumulation, and you swap only the piece that talks to your infrastructure. That covers durable runs, non-standard backends and custom resume endpoints without inheriting the maintenance cost of a hand-written client.
What is a resumable stream and does an agent UI need one?
A resumable stream lets a client reconnect to a generation that is still running on the server after the original connection closes. It needs a store such as Redis holding the stream, a record of which stream is active for a conversation, and an endpoint that replays from the last received position. You need it when runs are long enough that a user will refresh, switch tabs or lose signal mid-answer, which in practice means any agent doing multi-step tool calls. Short single-turn chat can skip it.
Does AG-UI replace the AI SDK?
No. AG-UI is a wire protocol that standardizes the event vocabulary between an agent backend and any frontend, with roughly sixteen event types covering text deltas, tool call lifecycle, state patches and lifecycle signals. An SDK is a client library that manages state and rendering. They sit at different layers, and a common production shape is an AG-UI event stream on the wire with an SDK or component library consuming it, which keeps a Python or Java agent backend from being coupled to a TypeScript frontend.
What is the difference between delta and snapshot events in a stream?
A delta carries only what changed since the last event, such as a token, a tool call opening or a result landing, and you append it. A snapshot carries the entire conversation so far on every emission, including messages rendered ten minutes ago, and you must reconcile it against what is already on screen. Handling a snapshot like a delta is invisible for text, because assigning the same string twice looks identical, and it breaks everything else: cards re-attach, charts redraw and tool calls re-announce every time the past comes round again.
Why does the same message render twice in a streaming chat UI?
Usually because the same content reaches the renderer by two correct routes at once, or because a snapshot re-delivered history the handler treated as new. Fixes that hold: bound any scan to messages after the last human message so old results cannot be acted on, seed a set of already-rendered element IDs from thread history so a page reload cannot re-offer them, and collapse duplicate extraction paths into a single function with a mode parameter rather than a comment telling future maintainers not to call both.
How many code paths lead to a rendered agent turn?
In practice four, not one. A fresh send watches the turn from the start and reconstructs nothing. A resume opens a thread while a run is in progress and rebuilds everything already emitted. A reconnect follows a page reload mid-run and rebuilds the same thing from a cold store with no in-memory state. A post-approval resume picks up a half-finished turn after a paused tool call is approved. Each route reimplements every rendering rule and they drift apart, so the number of routes should be decided as a design input before the first handler is written.
How do you measure whether agent streaming is good?
Track four numbers. Time to first byte tells you whether the connection opened. Time to first meaningful event tells you when the user saw something other than a spinner, which is the number that matters. The 95th percentile gap between events exposes proxy buffering and stalls. Resume success rate, meaning the share of interrupted streams a client rejoins without losing content, tells you whether your durability story is real. Tokens per second is a model metric and rarely the bottleneck in perceived speed.
Sources & further reading
Every external claim traces to a specification, vendor documentation or original research. The failure accounts in Part 5 are first-party, from Eerly's own assistant surface.
- WHATWG. HTML Living Standard, section 9.2 Server-sent events. The normative spec for the event-stream format, EventSource, reconnection and Last-Event-ID. html.spec.whatwg.org
- MDN Web Docs. Using server-sent events. Source of the six connection per origin ceiling on HTTP/1.1. developer.mozilla.org
- Anthropic. Streaming Messages, Claude Platform Docs. The Messages API event sequence, delta variants and cumulative usage. platform.claude.com
- Anthropic. Fine-grained tool streaming, Claude Platform Docs. Why accumulated tool input is not valid JSON until the block closes. platform.claude.com
- Vercel. Chatbot Resume Streams, AI SDK Documentation. The resume option, Redis-backed resumable streams and the 204 response. ai-sdk.dev
- Vercel. Abort and resumable streams, AI SDK Documentation. Why a client abort is a disconnect rather than a cancellation. ai-sdk.dev
- Vercel. AI SDK 5 (July 2025). Typed chat integration and the move to native server-sent events. vercel.com
- AG-UI Protocol. Introduction. An event-based protocol for agent to frontend interaction, with roughly sixteen event types. docs.ag-ui.com
- Nginx. Module ngx_http_proxy_module. proxy_buffering is on by default and disabled per response via X-Accel-Buffering. nginx.org
- Nielsen, J. Response Time Limits. Nielsen Norman Group. The 0.1, 1 and 10 second thresholds. nngroup.com

Kinal is a frontend architect specializing in large-scale web architecture, design systems and web performance. She writes about bridging complex frontend ecosystems with scalable engineering practices, from micro-frontends and state management patterns through to CI/CD and Core Web Vitals optimization.