- We fail the vocabulary test. The event stream is our product surface, so the hook's fixed model is the constraint rather than the help.
- A chat hook is an ORM over the conversation. Excellent while your access pattern is ordinary. Expensive the moment it is not.
- Two of our seven event types map cleanly onto the hook's model. The other five land in the generic data channel, outside the state machine we would be adopting it for.
- “SDK or our own” is the wrong unit. It is five decisions, and the honest answer differs per layer.
- One row of the standard failure list is not ours: our stop endpoint aborts generation server-side, so cancellation is real rather than cosmetic.
Part 1 · The decision
The test we fail
Zero failures and the SDK wins without a bake-off. We do not have zero.
The three tests are transport, durability and vocabulary. Ours is the third. Our stream is not a decorated transcript, it is the product surface. It carries approval gates that pause a run mid-flight, provenance attached to individual claims, and components the agent chooses at runtime. When the event vocabulary is the thing you sell, a library that fixes that vocabulary is not saving you work. It is setting your ceiling.
What follows is not a rebuttal of the failure list. That list is accurate, and most of it is ours either way. It is an argument about which layer we were being asked to give up, and what it would have cost us.
What is the vocabulary test?
A chat hook is an ORM over the conversation. While your access pattern is ordinary CRUD it is a gift. The moment it is not, you spend your days coaxing the ORM into emitting the SQL you already knew how to write.
The analogy we kept coming back toNobody regrets an ORM on a CRUD app. People regret one on a reporting workload, and the regret always arrives the same way: not as a wall, but as a slow accumulation of escape hatches until the abstraction is carrying none of the weight and all of the constraints. That is the shape we were looking at.
Part 2 · The mismatch
Where the shapes stop lining up
Our backend emits a run. The hook models a message.
A run is made of steps, approvals and attached sources. A message is made of parts. Two of our event types have a natural home in that model: token becomes a text part, tool.call becomes a tool part. The remaining five go through the generic data channel, untyped, unmanaged, and outside the state machine that is the hook's main offer.
The adapter that translates our events into parts and back out again has no value to a customer; it exists to satisfy the library. Writing it is not a shortcut we declined. It is a layer we would have been adding.
Split those seven by what they do and the gap stops looking like a rounding error. Everything the library manages for us is output. Everything that makes the interface ours is lifecycle or interaction, and all of it lands in the same untyped bucket.
Part 3 · The unbundling
It is five decisions, not one
“SDK or our own” is the wrong unit, and we agree with the original piece about that.
Draw the stack and the argument gets more precise. Rendering was never the hook's job. Transport and resumption can be replaced on their own. What the hook really sells sits in two layers: provider normalization, and the client state machine.
Part 4 · The case
Three reasons we keep ours
Three, not a list of seven. The rest were preferences dressed as arguments.
- We would be translating our model into theirs, permanently. The hook owns the message list, so its shape becomes our shape. Every new event type we invent starts life as a question about how to smuggle it through the data channel.
- No upgrade tax, and the backend keeps the contract. These SDKs have already changed their wire format once across a major version. With our own handler the backend defines the vocabulary and the frontend consumes it, so our roadmap is not coupled to someone else's release schedule.
- Failures land in code we wrote. A misbehaving stream gives us a stack trace in our own reducer rather than an unexpected intermediate state in one we did not write. Time-to-diagnose beats lines-of-code saved.
One correction to the standard list while we are here. The failure where a stop button halts the interface while the server keeps generating is a real bug, and it is not ours: our stop path hits a real endpoint that aborts the run, so cancellation stops the tokens rather than just the paint. Refresh and stop are separate routes, on purpose.
Part 5 · The other side
When the hook is the right call
Most teams reading this should install the library. We are the exception, and exceptions should have to earn it.
If the conversation is your product, rather than a window onto some longer job the backend is running, the hook already models nearly everything you emit. Text, tool calls, errors, retries: typed and reconciled before you touch them. Support assistants and documentation copilots live here. The work you skip is real, dull, and worth weeks.
None of the standard failure list stops applying to us, by the way. Partial token flushing, reconnection, scroll anchoring, error recovery: ours to fix, one at a time, instead of installing something that fixes them. That is a real cost. We took it on purpose.
Part 6 · The method
Run the test on your own stack
An afternoon at a whiteboard beats a week of opinions.
- Write down every event your backend emits, including the ones already sitting on next quarter's roadmap. Start from that list rather than a feature table.
- Map each event onto the library's model. First-class part, or generic channel. Count both columns before anyone starts arguing.
- Score transport, durability and vocabulary. Zero failures, install the library and go to lunch. One failure, work out which layer it lives in.
- Split the stack. Rendering, state machine, provider translation, transport, model output. Ownership differs per layer, and teams that skip this step end up issuing one verdict for five questions.
- Price the adapter. If it is permanent, invisible to customers, and grows every time you add an event, subtract it from whatever the library promised to save you.
Do that and the argument tends to die on its own. Most teams find zero failures and a clean map, and the library wins before lunch. A few find their own five-of-seven, and by then nobody is arguing about taste.
The library is good. That was never the question. The question was whether the model it manages is the model we are building, and ours is not it.
Eerly AI StudioThe stream should carry the decision, not just the transcript.
Eerly AI Studio streams approval gates and per-claim provenance across the systems your teams already run, inside the permissions those systems already enforce.
Book a demo →FAQ
Frequently asked questions
The questions engineers ask us most often about streaming architecture, and the short answers.
Should you build your own streaming handler or use a chat SDK?
Use the SDK unless your event vocabulary is part of what you sell. Three tests settle it: transport, durability, vocabulary. Ordinary wire protocol, no need to recover a run after a refresh, stable event set, and you should install the library and stop debating. Fail one of them, vocabulary above all, and the library's fixed message model becomes a constraint you carry for as long as the product lives.
What is the vocabulary test?
It asks whether your stream is a decorated transcript or the product itself. A stream that pauses a run for approval, pins sources to individual claims and hands back components the agent picked while it ran is not a transcript. Once the vocabulary is the thing you sell, a library that fixes it sets your ceiling.
When is a streaming SDK clearly the right call?
When the conversation is the product rather than a window onto a longer job the backend is running. Support assistants and documentation copilots emit text, tool calls and errors, which the library already models and reconciles. Building your own handler there costs weeks of edge cases around partial tokens, aborted requests, scroll behaviour and error recovery, and buys nothing a user would notice.
Why is a chat hook like an ORM over the conversation?
Both swap direct control for a managed model. While your access pattern is ordinary the trade is a gift. Once it is not, you spend your days coaxing the abstraction into emitting what you already knew how to write. The regret rarely arrives as a wall. It shows up as escape hatches piling up until the abstraction carries none of the weight and all of the constraints.
What breaks when custom events go through a generic data channel?
They sit outside the state machine you adopted the library for. Nothing types them, nothing reconciles them across a reconnect, they do not move the library's status transitions, and every consumer re-derives meaning from a raw payload. You keep the constraints of the managed model and lose the management for the events that differentiate you.
Can you keep an SDK for transport and still own the client state machine?
Sometimes. Transport with resume and cancel swaps out on its own, and rendering is yours either way. The layer you cannot swap independently is the state machine, because it defines the shape of the message list. If that is where you diverge, the middle path does not save you.
Does writing your own streaming handler mean inheriting the usual streaming bugs?
Yes, most of them, and any honest build-or-buy argument says so up front. Partial token flushing, aborted requests, reconnection, scroll anchoring, error recovery: all yours. Cancellation is the one worth checking case by case, since it is a backend question rather than a library one.
How do you make a stop button actually stop generation?
Aborting the client request only stops the paint. To stop the tokens you need a server route that ends the run: the client calls stop with the run ID, the server aborts its own request to the model provider, and the run gets marked cancelled in storage so a later reconnect does not resume something the user already killed. Keep resume and stop as separate routes.
Notes & further reading
- This is a response post. The original argument for adopting the SDK, including the three tests and the failure list referenced throughout, is published alongside it: Streaming in Agent UIs: Use the SDK or Roll Your Own?
- The event counts, mappings and layer ownership here describe the Eerly AI Studio streaming stack in August 2026. It is an account of one codebase, not a benchmark.
- MDN Web Docs. Using server-sent events. Background on the transport layer discussed in Part 1. developer.mozilla.org
- MDN Web Docs. AbortController. The client half of cancellation, and the reason the server half is the part that matters. developer.mozilla.org

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.