Yashveer Singh
Connect
<- All posts

Streaming AI Responses to Users: An Architecture Primer

Streaming AI responses is a UX decision with real backend consequences. Here is how to implement it without making your product unreliable.

Written by Yashveer Singh, founder of Yashveer Labs.

# Streaming AI Responses to Users: An Architecture Primer

Streaming AI responses means sending the model's output token by token as it is generated, rather than waiting for the full response before showing anything. It makes AI-powered features feel dramatically faster even when the total generation time is the same. It is also a meaningfully different architecture than returning a JSON response, and building it correctly requires understanding the tradeoffs before you start wiring it up.

What you need to know

  • Streaming requires Server-Sent Events or WebSockets on the frontend, not a standard fetch-and-await pattern
  • Your backend needs to support streaming explicitly; most frameworks do, but the implementation varies by runtime
  • Error handling is harder with streaming because the error can occur mid-stream, after you have already sent a 200 status
  • Streaming adds complexity around rate limiting, request cancellation, and cost tracking that static responses avoid
  • The user experience benefit is real: perceived speed matters more than actual speed for most AI-powered interactions

The core argument

When you call an LLM API and wait for the full response before displaying it, users stare at a loading spinner for three to twenty seconds depending on the prompt length. When you stream the response, characters appear on screen within milliseconds of the model starting to generate. The total generation time is identical. The perceived experience is completely different. This is not a small UX nicety. In my experience building AI features, the streaming version consistently outperforms the non-streaming version on engagement and completion metrics. Users who see the response starting to form are more likely to read it all the way through.

The architecture shift is real. A standard API endpoint accepts a request and returns a response. Streaming requires a long-lived connection where data flows in one direction over time. In Next.js with the App Router, you handle this using ReadableStream and Response. In Node.js with Express or Fastify, you pipe the LLM SDK stream directly to the HTTP response. In all cases, the connection stays open until the model finishes, and the client reads data as it arrives. This works fine for most users. It requires care when you have high concurrency, because long-lived connections consume server resources differently than fast request-response cycles.

The error handling problem is the part most teams underestimate. With a standard response, you catch the error and return a proper status code. With streaming, you have already sent a 200 status and potentially several tokens before the error occurs. If the model hits a rate limit midway through a response, or if your context window runs out, you cannot retroactively send a 500. You need to send a special sentinel value in the stream to signal an error to the client, and the client needs to handle it gracefully. I had to build exactly this for a feature in Nexli, and the edge case testing took twice as long as the initial implementation.

Common mistakes

  1. Not handling mid-stream errors. If you do not send an error signal in the stream, the user sees a truncated response with no indication that something went wrong. Build a protocol for error tokens from the start.
  2. Streaming on routes with short responses. Streaming adds overhead. For responses under 100 tokens, the streaming setup cost often makes the experience feel worse, not better. Use streaming for long-form generation only.
  3. Not canceling the upstream request when the user navigates away. If a user clicks away mid-generation, your server is still paying for tokens being generated into nothing. Use AbortController on the frontend and propagate cancellation to the LLM API call.
  4. Not accounting for streaming in your cost tracking. Standard token counting works fine for batch responses. For streaming, you need to count tokens as they arrive or use the usage metadata the LLM provider appends at the end of the stream.
  5. Ignoring backpressure. If the client cannot consume the stream as fast as the server produces it, you need buffer management. Most Node.js streaming implementations handle this, but ignoring it causes memory issues at scale.

Where to start

Step 1: Implement streaming on a single, low-stakes AI feature first. Do not stream your most complex endpoint first. Start with a simple text generation feature, get the full error handling and cancellation working, then apply the same pattern to more complex routes.

Step 2: Build a streaming client hook or utility before you need it in multiple places. A reusable useStream hook in React or an equivalent utility handles the EventSource or fetch-with-streaming logic once and shares it across features. Building this at the start saves significant duplication later.

Step 3: Test mid-stream errors explicitly in your local environment. Add a flag to your backend that can force an error after N tokens. Run this in testing to confirm your frontend handles it gracefully. Most teams skip this test until a production incident forces their hand.

Related reading

FAQ

Frequently asked

Author

About the author and why it matters

Yashveer Singh wrote this. I run Yashveer Labs out of New Delhi. The work I take on tends to come from founders who have been burned by an agency, a freelancer, or their own ambition. I do not promise miracles. I promise that the system will be online, the code will be readable, and the next engineer who touches it will not curse me. That is rarer than it should be.

Related reading