Agent Engineering · Frontend Architecture

    Streaming in Agent UIs: Use the SDK or Roll Your Own?

    Getting tokens onto a screen takes an afternoon. Surviving a refresh, a proxy, a cancelled run and the same turn arriving twice by a route you did not plan for takes considerably longer. Here is how to decide which of those problems you want to own.

    August 20, 2026 | 13 min read

    Key takeaways
    • 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?

    The streaming contract is the set of guarantees an agent interface holds between the model's event stream and the screen: events arrive in order, the same turn delivered twice renders once, partial state renders without lying, a disconnect does not destroy work in progress, cancellation actually cancels the generation on the server, and every rendered claim traces back to the event that produced it. Every agent UI has this contract whether or not anyone wrote it down. Choosing an SDK is choosing who maintains it.
    6
    concurrent HTTP/1.1 connections a browser allows per origin, and one open stream occupies a slot until it closes (MDN)
    ~16
    standard event types in the AG-UI protocol, covering text deltas, tool lifecycle, state patches and handoffs (AG-UI)
    1 s
    the classic threshold past which a user notices the wait and stops feeling in control of the interface (Nielsen Norman Group)

    An honest answer to the build-or-buy question needs both columns of the ledger, so start with what one turn actually costs.

    Anatomy of one streamed agent turn and where it breaksA single user message travels left to right through six stages: the send event, the HTTP request, the model event stream, server normalization, the wire including proxies and CDNs, and finally the client render. Each stage carries a common failure annotation. Two bars below compare a buffered response, blank for twenty eight seconds, against a streamed response with first paint at one point one seconds, with the ten second attention limit marked.ANATOMY OF ONE STREAMED TURN · WHERE IT BREAKSone ordinary agent turn: a question, two tool calls, four hundred tokens of answerUSERSENDSt = 0REQUESTauth, route,cold startMODEL SSEmessage_start,deltas, pingNORMALIZEone event shapeacross providersTHE WIREproxy, CDN,load balancerCLIENTRENDERt = 1.1s+ 400ms cold start+ partial JSON+ schema drift+ buffered 8KB+ re-render stormFIVE STAGES. THE MODEL IS RESPONSIBLE FOR EXACTLY ONE OF THEM.TIME THE USER SPENDS LOOKING AT NOTHINGBUFFERED REPLY28s of empty panel, no signal the run is alive, two tool calls invisible28sSTREAMED REPLY1.1s to first paint, then tool names, arguments and text arrive live1.1s10s: attention starts to drift (NN/g)Total generation time is identical in both bars. Only the contract between the stream and the screen changed.eerly.ai
    Figure 1. Both rows finish at the same instant. The buffered row spends twenty eight seconds convincing the user the product is broken, which is why streaming is a correctness requirement in agent interfaces rather than a flourish.

    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 Studio

    Part 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.

    Two ledgers: the streaming happy path versus the streaming failure listLeft panel shows the happy path: roughly fifty two lines of code, one afternoon of work, one provider supported. Right panel plots cumulative engineering days against the number of production failure modes handled, rising from about two days at two failures to roughly forty five days at eleven, crossing the one sprint line between four and five failure modes.THE HAPPY PATHwhat a streaming prototype costs to write~52lines: parse SSE, append deltas,flush the response, render1 dayto a demo that streams convincinglyon localhost, one browser, one tab1provider, one event schema, zeroreconnects, zero cancellationsThis is the estimate that gets put in the ticket.✓ ACCURATE, AND IT DESCRIBES 10% OF THE WORKline count: minimal SSE reader plus a React reducer, no error handlingTHE FAILURE LISTcumulative engineering days, by failure modes handledpast one full sprint60 d40 d20 d01 d9 d37 d45 d1357911production failure modes handled properlyREAD TOGETHER:the prototype is linear. Hardening it is not, and the curve is what the SDK amortizes.eerly.ai · curve illustrative, drawn from the failure list in Figure 3
    Figure 2. Teams estimate the left panel and ship against the right one. The gap between them is where custom streaming projects quietly become a permanent internal library with one maintainer.

    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.

    Streaming failure modes, symptoms and who handles themA table of seven common streaming failure modes with the layer each belongs to, the symptom a user sees, and whether a mainstream SDK covers it. Proxy buffering and HTTP connection limits sit in infrastructure and are not covered by any SDK. Reconnection, abort semantics, partial tool JSON and usage accounting are covered. Markdown flicker and backpressure are partly covered.THE FAILURE LIST, AND WHO IS ON THE HOOKan SDK narrows this table. It does not empty it.WHAT BREAKSLAYERWHAT THE USER SEESSDK COVERS IT?Proxy bufferingInfrastructureNothing for 30s, then the whole reply at onceNO · your config6-connection capTransportSeventh tab hangs, images stop loadingNO · serve HTTP/2Refresh mid-runSessionHalf an answer disappears, tokens already paid forPARTLY · needs RedisStop vs disconnectControl planeStop button stops the UI, server keeps generatingPARTLY · needs endpointPartial tool JSONEvent parsingBroken braces flash on screen, then vanishYESMarkdown flickerRenderCode fences open and close, lists reflow, math jumpsRARELYSlow-client backpressureServerMemory climbs, streams stall, pod restartsNOCumulative usage countsAccountingBilling dashboard reports 6x the real token spendYESTHE ROWS AN SDK CANNOT COVER ARE INFRASTRUCTURE ROWS. THEY ARE YOURS EITHER WAY.Not shown: unknown event types crashing a client, duplicate renders on reconnect, and stale message IDs after regeneration.eerly.ai
    Figure 3. Building your own transport does not delete a single row. It moves four of them from a maintained dependency onto your backlog, and leaves the infrastructure rows exactly where they were.

    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 Engineering
    Delta events versus snapshot events, and how snapshots re-deliver historyLeft panel contrasts two event types: a delta carries only what changed and should be appended, while a snapshot carries the whole conversation every time and must be reconciled against what is already rendered. Right panel is a grid showing three successive snapshots against three messages: the first snapshot delivers message one, the second re-delivers message one and adds message two, and the third re-delivers both and adds message three, so message one arrives three times in total.TWO EVENTS, ONE SWITCH STATEMENTthey look equally innocuous. They are opposites.DELTAcarries only what changed since the lastevent: a token, a tool opening, a resultAPPEND, MOVE ONSNAPSHOTcarries the entire conversation so far,including what you rendered ten minutes agoRECONCILE, OR RENDER IT TWICE✓ INVISIBLE FOR TEXT✗ CATASTROPHIC FOR EVERYTHING ELSEWHAT EACH SNAPSHOT DELIVERSas a thread grows, the past keeps arrivingMESSAGE 1MESSAGE 2MESSAGE 31st snapshotnew2nd snapshotagainnew3rd snapshotagainagainnewMESSAGE 1 ARRIVES THREE TIMES.Nothing in the payload marks which parts you already rendered.Your handler needs an external notion of "now": bound the scan, andseed the dedupe set from history so a reload cannot re-offer old work.CLASSIFY BEFORE YOU HANDLE:append or reconcile. Every other decision in this article depends on that one.eerly.ai
    Figure 4. The right panel is the whole bug in one grid. A renderer that treats every arriving message as new will re-attach a card, re-draw a chart and re-announce a tool call every time the past comes round again.

    Count 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.

    Four routes into the same rendered turnA table of four code paths that produce one visual outcome. Fresh send watches the turn from the start and reconstructs nothing. Resume opens a thread while a run is in progress and must rebuild everything already emitted before attaching to live deltas. Reconnect follows a page reload mid-run and rebuilds the same thing from a cold store with no in-memory state. Post-approval resumes a paused tool call and must rebuild the half-finished turn. Each route independently reimplements every rendering rule.FOUR ROUTES, ONE RENDERED TURNwe planned for one of these. The other three arrived as tickets.ROUTEHAPPENS WHENWHAT IT MUST RECONSTRUCTSTATE AVAILABLEFresh sendthe user submits a messagenothing, it watches from the startin memoryResumea thread opens mid-runeverything emitted, then live deltaspartialReconnectthe page reloads mid-runthe same, from a cold storenonePost-approvala paused tool call is approvedthe half-finished turn, then the restpartialEVERY RENDERING RULE GETS WRITTEN ONCE PER ROUTE, WHETHER YOU PLANNED FOR THAT OR NOT.The differences are not accidental duplication. Each one encodes a product decision, which is why the unifying refactor stalls.Symptom we shipped for months: reconnect rendered tool activity without tool names.eerly.ai
    Figure 5. Four handlers, one visual outcome. The durability test in the next section is really asking how many rows this table has for your product, because each row is a place the streaming contract has to hold independently.

    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.

    Transport comparison for agent interfacesA comparison matrix of four transports for agent UIs: server-sent events, WebSockets, long polling and durable or replayable streams. Rows cover direction, proxy friendliness, automatic reconnect, resumability, binary support and typical use. SSE scores well on proxy friendliness and automatic reconnect and is marked the sane default. WebSockets win on bidirectional traffic and binary frames. Long polling is a fallback. Durable streams add replay at the cost of infrastructure.PICKING THE PIPE · FOUR TRANSPORTS, ONE DEFAULTthe industry converged here for reasons that survive contact with an enterprise networkPROPERTYSSE / HTTPWEBSOCKETLONG POLLDURABLE STREAMDirectionserver to clientboth waysrequest pullserver to manySurvives corporate proxies✓ plain HTTP✗ often blockedReconnects without code✓ EventSource✗ you write itReplays what you missedonly with a storeonly with a store✓ by designBinary frames✗ text onlydependsOperational costlowsticky sessionschatty, jerkyRedis plus state▲ START HERERULE OF THUMB:SSE for the data plane. Add WebSockets only when the client needs to push mid-run.eerly.ai
    Figure 6. Most teams that chose WebSockets first arrived back at SSE after their first enterprise deployment, because the column that decides this is proxy behaviour rather than protocol elegance.

    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 Studio
    The streaming stack: five layers and who owns each oneA stacked architecture diagram read from the bottom up. Layer one is the provider event stream from Anthropic, OpenAI or Gemini. Layer two normalizes provider events into one vocabulary. Layer three is transport and resumption covering SSE, the active stream record and replay. Layer four is the client state machine handling message identity, status and tool accumulation. At the top is the render layer with streaming-safe markdown, tool cards and provenance. Annotations mark which layers an SDK covers and which the team always owns.THE STREAMING STACK · UNBUNDLE IT BEFORE YOU CHOOSEyou can buy layers 2 and 4 while owning layer 3. Read the stack upwards.5 · RENDER, HONESTLYstreaming-safe markdown, tool cards that show arguments arriving, provenance on every claimDEBOUNCE 50msOPEN FENCES OKALWAYS YOURS. NO SDK SHIPS YOUR UX.4 · CLIENT STATE MACHINEmessage identity, submitted / streaming / ready / error, tool accumulation, stop and regeneratebuy this3 · TRANSPORT & RESUMPTIONSSE out, active-stream record per conversation, replay from last position, real cancel endpointswap this layer first2 · EVENT NORMALIZATIONone vocabulary across providers, unknown event types ignored rather than fatal, usage counted oncebuy, or adopt AG-UI1 · PROVIDER EVENT STREAMmessage_starttext_deltainput_json_deltathinking / citations / pingSkip layer 3 and a refresh destroys work you already paid for. Skip layer 5 and the interface renders a half-built object as fact.eerly.ai
    Figure 7. The build-or-buy argument usually collapses once a team draws this stack, because the honest answer is different per layer and nobody was disagreeing about the same one.
    Operational takeaway
    Write the contract down before you pick the library
    Four sentences, in a document: what happens on disconnect, what happens on stop, what a half-finished tool call is allowed to render, and how a rendered claim links to its source event. Teams that write those four sentences first rarely argue about SDKs afterwards. Teams that skip them rewrite their streaming layer twice.
    Eerly AI Studio

    Agent 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.

    A four band decision scale for agent streaming architectureA four band scale from SDK default, through SDK with a custom transport, to a shared protocol such as AG-UI, to a fully custom stack. Each band lists what it suits and what it costs. Most teams belong in the first two bands, and the arrow beneath shows control increasing left to right while maintenance cost increases with it.WHERE YOUR PROJECT BELONGSnot "which library is best" but "how many of the three tests did you fail"▼ most teams belong hereSDK DEFAULTFAILED 0 TESTSSWAP TRANSPORTFAILED 1 TESTSHARED PROTOCOLFAILED 2 TESTSFULL CUSTOMFAILED ALL 3useChat and a routehandler, ship this weekkeep the state machine,own resumption and replayAG-UI on the wire, backendand frontend stay decoupleda permanent internal libraryand the person who maintains itCONTROL INCREASING → AND MAINTENANCE COST WITH ITeerly.ai
    Figure 8. Place your project honestly. Every band on this scale is defensible, and the expensive mistake is landing in the fourth one by accident because nobody ran the three tests at the start.

    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 Studio

    FAQ

    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 Kukda
    Written by
    Kinal Kukda
    Frontend Architect, Eerly.ai

    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.