The wait is the interface
Past a few seconds the spinner stops being a loading state and becomes the product — a request/response chatbot and a streaming one are different applications, not the same application with a different animation.
The points-and-miles chatbot answers a real question: what is the cheapest way to get two people from Austin to Tokyo in business class in March, using the points I already have? Answering it properly means checking award availability across a few programs, working out which transfer partners reach them, and doing the arithmetic on what is actually in the account. It is a genuinely good answer when it arrives.
It arrives all at once, after a wait, as a wall of text. During that wait the interface offers one thing: a spinner. The user cannot tell whether it is thinking, stuck, or already finished and rendering. They cannot stop it. If they switch tabs and come back, they get whatever the browser happened to preserve. If it fails at 80 percent, they get nothing and start over.
None of that is a bug. It is what request, wait, render looks like when the wait grows from 300 milliseconds to forty seconds. The architecture did not change. The product did.
Three numbers, and the point where they run out
The response-time thresholds everyone half-remembers come from Jakob Nielsen, and they are older than most of the web: 0.1 second is “the limit for having the user feel that the system is reacting instantaneously”; 1.0 second is “the limit for the user’s flow of thought to stay uninterrupted”; 10 seconds is “the limit for keeping the user’s attention focused on the dialogue” (Nielsen, 1993). NN/g later split the middle band into a rendering rule: a looped animation for 2–9 seconds, a percent-done indicator for 10 or more (Sherwin, 2014). Nielsen Norman Group sells UX training and research reports, so this is a consultancy publishing its own foundational work — but it is the primary source for these numbers, not a summary of someone else’s.
Read the ten-second rule carefully, because it does not say “users get impatient.” It says attention leaves. Nielsen is explicit about the consequence: past ten seconds “users will want to perform other tasks while waiting for the computer to finish, so they should be given feedback indicating when the computer expects to be done”. That is not a styling note. It is a statement that the user will physically go somewhere else, and the interface has to survive their absence.
Now notice where the guidance stops. The prescribed fix above ten seconds is a percent-done indicator — and a percent-done indicator requires knowing the denominator. A file upload knows its byte count. A report generator knows its row count. An agent answering the Tokyo question does not know how many tokens the answer will be, how many tools it will call, or whether the third tool call will come back empty and send it down a different path. There is no honest denominator.
Streaming does not make it faster
This is worth stating flatly because the pitch usually implies otherwise. Streaming the Tokyo answer does not reduce the time until the answer is complete. If anything it adds a small amount of overhead. What it changes is when the first piece arrives — and that one change cascades into a different application.
First content at 900 milliseconds instead of forty seconds buys three things that no spinner provides. It proves the system is alive, continuously, rather than asserting it once. It gives the user something to do during the wait — reading is a task, waiting is not — which is the mechanism by which streaming beats the ten-second attention limit rather than merely decorating it. And it lets the user judge direction early: two sentences in, they can tell the agent misread “business class” as “business trip” and kill it, instead of discovering that after forty seconds of generation they already paid for.
There is also a blunt infrastructural reason, and it comes from the model vendors themselves. Anthropic — documenting its own API, so read it as such — warns against long non-streaming requests on the grounds that “some networks may drop idle connections after a variable period of time, which can cause the request to fail or time out without receiving a response,” and states that its SDKs “validate that your non-streaming Messages API requests are not expected to exceed a 10-minute timeout”. A long request that produces nothing on the wire looks identical to a dead connection, to every intermediary between you and the model. A stream is, among other things, proof of liveness that middleboxes can see.
And one detail from that same page that will matter for the rest of this course: “an error can occur after the API returns a 200 response. In that case, error handling doesn’t follow these standard mechanisms”. Once you stream, the HTTP status code stops being where failure lives. Your error handling moves inside the response body. That is not a streaming inconvenience; it is a structural change to how the client knows anything went wrong.
What the long wait forces you to build
Here is the concrete inventory. Each of these is a control the two-second version of the chatbot never needed, and each one is absent from the current build:
- Continuous proof of life. Not “a request is in flight” asserted once at submit, but evidence arriving on an ongoing basis. A spinner cannot distinguish working from hung; a stream can, because it stops.
- Narration of what it is doing. “Checking award space on ANA” is a different claim from “loading,” and it is the claim that makes a forty-second wait tolerable. This is only possible because an agent stream carries more than text.
- An exit. Nielsen’s ten-second rule says attention will leave. If it leaves and cannot come back — or leaves and the work keeps billing — that is a design failure, not user error.
- Survivable absence. The user will switch tabs. The question is what they find on return, and today the honest answer for most streaming builds is “whatever the browser happened to keep.”
- A way to tell finished from still-going. When text appears incrementally, a pause is ambiguous: thinking, calling a tool, rate-limited, or done. The end of a stream must be rendered as an event, not inferred from silence.
- In-band failure. Because the status code is already 200, the UI needs a rendering for “this answer stopped being produced” that is distinct from both “done” and “still coming.”
Notice that only the first two are about making the wait pleasant. The other four are about the wait failing — which is the half that every streaming demo skips, and the half that decides whether a surface can be handed to somebody else’s product team.
Where people get burned
The trap in this material is that a happy-path streaming chatbot takes an afternoon. Tokens appear, it looks alive, it demos beautifully, and it feels finished. Every item in the list above is invisible in that demo, because a demo is short, on a fast connection, watched by someone who will not switch tabs and has no reason to press stop. The gap between that afternoon and a streaming surface you can graft into ninety portfolio-company products is entirely the list you just read.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
The current chatbot takes forty seconds and shows a spinner. Name the thing that is wrong with it that is NOT “the wait is too long.”
Check your answer
The wait length is a fact about the work, not a defect — a good answer to the Tokyo question genuinely requires several lookups. Shortening it is a backend problem and mostly not available to you.
What is wrong is that the interface offers no way to act during a period it has already conceded the user will not spend watching. There is no way to see it is alive, no way to see what it is doing, no way to stop it, and nothing correct that happens if the user leaves and returns. A forty-second wait with those four things is a reasonable product. A four-second wait without them is fine only because nothing has time to go wrong.
Hands on
Measure the baseline before you change anything
Done when: ARTIFACT.md’s Baseline section is filled in with numbers you measured today — not remembered — and carries a written list of the affordances the current UI does not have.
- Open
learning/streaming-ui/ARTIFACT.md. The Baseline section is seeded and empty on purpose. - Run the chatbot against a genuinely hard question — one that forces multiple lookups, like the two-passengers-to-Tokyo case, not “what is a transfer partner.” Record time from submit to first visible content and time from submit to complete answer, in milliseconds. Use the network panel, not a stopwatch. Do it three times and record the worst.
- Record the longest response you can provoke, and the number of tool calls in a typical turn. If you cannot see the tool-call count from the outside, that itself is a finding — write down that the current UI cannot observe it.
- Fill in the three failure rows honestly: what the user sees during the wait, what happens if they navigate away mid-request, and what happens if the request fails at 80 percent. Test the last two rather than predicting them. The answer to “navigate away” is usually more interesting than expected.
- Record what sits in front of the deployed app — CDN, platform edge, reverse proxy, corporate gateway, or nothing. Do not guess; look at the response headers. The transport lesson’s hands-on task depends on this answer and it is the field most likely to be filled in from memory and be wrong.
- Under the baseline, write the six-item affordance list from this lesson and mark each one present or absent in the current build. Then bring the filled section into the chat. I will push on any number that looks remembered rather than measured, and on any “it probably just…” in the failure rows.
What this does not cover
This lesson argues that the surface has to stream and names what streaming obliges you to build. It says nothing about how the bytes actually get to the browser — and that choice is not a detail, because the transport you pick on localhost is chosen for reasons that stop applying the moment a buffering reverse proxy sits in front of it. The transport lesson takes that on directly: SSE, WebSocket, chunked HTTP and polling, scored on how each one behaves in somebody else’s infrastructure rather than on yours.
The narration item in the affordance list is also a promise this lesson cannot keep on its own. Saying “checking award space on ANA” requires the stream to carry tool calls as first-class events, not text — which is what the agent-stream lesson at the end of this module builds, and what the raw-events lesson before it makes visible.
Read this next — primary source
Response Time Limits: Article by Jakob NielsenJakob Nielsen, Nielsen Norman Group, January 1, 1993 — free; NN/g sells UX consulting, training and research reports, so read it as an interested party publishing its own foundational research
The lesson takes three numbers from it. The article itself is about a page long and worth reading whole because of what surrounds them: Nielsen is explicit that the limits come from human perceptual and cognitive constants rather than from any technology, which is exactly why a thirty-three-year-old article still governs a model that did not exist when it was written. It also states the reasoning for the ten-second rule — that past it users start doing something else — which is the sentence that turns a latency number into an interaction-design requirement.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.