LLM streaming latency is the delay between sending a prompt and seeing text appear, and the number that decides whether an AI chat feels fast is time to first token rather than total response time.

Two answers can take the same 8 seconds. One starts writing in 400ms and finishes gradually. The other shows a spinner for 7 seconds then dumps a wall of text. People describe the first as fast and the second as broken, and they are the same duration.

Diagram of the ChatFuse streaming pipeline: an SSE consumer parses events, a smoothing buffer releases text at word boundaries, and only the streaming message re-renders.

Getting the first case takes more than turning streaming on.

What makes LLM streaming latency feel slow?

Uneven delivery, not total duration. Tokens arrive from the network in bursts, and if you render each burst the moment it lands, text appears in lumps of 30 words at a time with dead air between them.

That is worse to read than a steady crawl at the same average speed. The eye tracks a moving edge comfortably and stalls on a jump. So the delivery rate and the render rate need to be different things, which is the whole reason the buffer below exists.

How is the ChatFuse streaming pipeline structured?

ChatFuse splits it into 7 layers, each owning one job, so no layer has to know how the others work.

LayerJob
SSE consumerParse server sent events, dispatch typed callbacks
Streaming hookToken smoothing buffer, state, lifecycle
Content contextBypass the memo so only streaming updates flow
Message listVirtualization, scroll behavior, last item scoping
Message itemPer message logic and content resolution
Markdown rendererOne renderer for streaming and finished text
Markdown processingCitation conversion, canvas markup stripping

The rule that keeps this fast is that no markdown processing happens during streaming. Raw text goes straight to the renderer, and the parsing work only runs once the message is complete.

Why does a smoothing buffer beat rendering every chunk?

Because the ChatFuse buffer decouples how fast text appears from how fast the network delivers it, and the network is bursty.

Every chunk writes the full accumulated text into a ref, which triggers no re-render at all. Then one of two things happens. If fewer than 12 characters are waiting, we render immediately, so a slow model never gets an artificial delay stacked on top of it. If more than 12 are waiting, a 16ms interval starts and releases text at a steady pace.

The interval never splits a word. It advances to the next whitespace boundary before rendering, so you see whole words appear rather than fragments.

Does the ChatFuse buffer fall behind on a fast model?

No, because the release rate scales with how much is waiting.

Characters bufferedReleased per tick
Over 20040
100 to 20020
40 to 10012
Under 408

A thin buffer drips out at 8 characters per tick. A model dumping tokens faster than the interval can drain gets 40. When the interval catches up to the source it pauses itself rather than spinning.

Chart of the adaptive release rate: 8 characters per tick on a thin buffer, rising to 40 per tick when more than 200 characters are waiting.

Why not use requestAnimationFrame instead?

We did, and it produced the exact problem we were trying to solve.

An animation frame callback fires once per frame and renders everything buffered since the last one. When the network bursts, that is a lot of tokens at once, so the user sees a word blob appear rather than words arriving. Frame timing follows the display, and the reading experience should follow the text.

The interval approach is less elegant and it reads better, which settled it.

How do you stop the whole thread re-rendering on every token?

ChatFuse scopes updates so only the message currently streaming re-renders, and letting everything else stay put.

Streaming content flows through its own context that deliberately bypasses the memo boundary the rest of the thread sits behind. A conversation with 60 messages does the same work per token as a conversation with 2. Without that scoping, the cost of each token grows with the length of the thread, which is precisely backwards from what you want in a long chat.

Does the message flicker when streaming ends?

It should not, and avoiding it means using one renderer for both states.

The tempting design is a fast plain text renderer during streaming and a full markdown renderer once complete. That swap causes a visible reflow at exactly the moment the reader is finishing the last sentence. Instead a single renderer handles both, with a flag controlling whether it animates. The completion is a state transition rather than a component swap, so nothing re-lays out.

Swapping renderers at completionOne renderer with an animation flag
While streamingFast plain text rendererThe same renderer, animated
On completionFull markdown renderer takes overThe same renderer, animation off
What the reader seesA visible reflow mid sentenceNothing moves
What ChatFuse shipsNot thisThis

What happens when someone stops a response mid stream?

The ChatFuse cursor resets and the smoothing interval stops, which sounds obvious and is the part that broke twice.

If text gets cleared or replaced while the buffer still holds a position from the previous stream, the released slice is computed against text that no longer exists. So the handler checks whether the incoming text is shorter than what has already been rendered, and treats that as a reset rather than as progress.

Every piece of streaming cleanup lives in one place for the same reason. Cleanup scattered across an abort path, an error path, and a completion path drifts, and the state you end up in depends on which path you took.

How does scrolling keep up with the text?

The ChatFuse message list follows output with smooth scrolling while streaming, and stops following when the stream ends.

The subtlety is that following output has to be scoped to the last item. Apply it to the whole list and any late arriving content anywhere in the thread yanks the viewport. Scoped correctly, the view tracks the growing message and leaves you alone if you have scrolled up to reread something.

What should you measure instead of total time?

Three numbers, and total duration is the least useful of them.

MeasurementWhat it tells youWorth tracking
Time to first tokenHow long the reader stares at nothingMost
Worst gap between chunksThe stall they actually noticedUsually ignored, matters
Total durationOnly useful for comparing modelsLeast

Time to first token is how long the reader stares at nothing. It is the number that decides whether the product feels responsive, and it is almost entirely determined by which model you routed to and how much work happens before the request goes out.

The gap between rendered chunks is the one most teams never look at. Average it and everything looks fine. Take the worst gap in a response and you find the stall the user actually noticed, because people remember the pause, not the mean.

Total duration matters only for comparing models on the same task. It tells you nothing about the experience, which is why a chat that finishes later can be the one that feels quicker.

Is time to first token the same as latency?

No. Latency usually means the total round trip. Time to first token measures how long the reader waits before anything appears, and it is the part they experience as speed. A model that finishes 2 seconds sooner but sits silent for the first 5 will be described as slower by almost everyone who uses it.

This is also why routing matters. Sending a short prompt to a frontier model costs first token time you did not need to spend, which we covered in AI model orchestration.

Does streaming change what the model produces?

No. Streaming is a delivery mechanism, and the tokens are identical whether you stream them or wait for the complete response. Everything above is about when text reaches the screen, not what the text says.

How do I see ChatFuse streaming for myself?

Send something long enough to stream properly, like asking for an explanation rather than a fact. Start free and watch where the words appear, or read how the model gets picked in AI prompt classification.

Back to Blog

Written by Dan

Share

Comments

Loading comments…

Secure signup continues in a new tab.