Your API Isn't Slow. Your Response Strategy Is.
There's a class of bug that never shows up in your performance benchmarks but destroys user trust every single time: the frozen UI on a long-running request.
You've seen it. A user clicks a button. The spinner appears. Ten seconds pass. Fifteen. The page looks completely dead. Has it crashed? Is it working? Should they refresh? By the time the response arrives, the damage is already done — they've lost confidence in your app.
The instinct is to optimize the underlying operation. Faster queries, better caching, smarter algorithms. That's not wrong, but it's also not the whole picture. Sometimes the operation genuinely takes time, and the real fix isn't making it faster — it's changing what the user experiences while they wait.
The Response Model We've All Accepted Without Questioning
HTTP request-response is simple to reason about: client asks, server answers. But that model bakes in an assumption — that the server has everything ready before it says anything. For a 200ms database query, fine. For a multi-step pipeline, an AI completion, or a report that aggregates across dozens of sources, that assumption turns your UI into a black box.
The alternative is incremental delivery. Start responding the moment you have something meaningful to say, then keep sending as more becomes available. The total time doesn't change, but the user's experience of that time changes completely.
Server-Sent Events Are Underused and Underrated
WebSockets get all the attention for real-time patterns, but they bring genuine complexity: stateful connections, custom protocols, proxy headaches, and deployment constraints that don't play well with serverless or edge environments.
Server-Sent Events are different. They're HTTP. The server holds a response open and pushes text in a simple format. The browser handles reconnection automatically. There's no handshake protocol to manage, no separate connection to maintain. For server-to-client streaming — which covers most real-world use cases — SSE gives you 80% of the capability at about 20% of the complexity.
In Next.js App Router, this pairs naturally with ReadableStream to wire up an SSE endpoint without a custom server:
export async function GET() {
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const send = (data: string) => {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ chunk: data })}\n\n`));
};
send("Starting pipeline...");
await runFirstStage(send);
send("Stage one complete. Processing...");
await runSecondStage(send);
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
"Connection": "keep-alive",
},
});
}
The client listens with EventSource or a simple fetch with a streaming reader, and it starts rendering updates the moment the first chunk arrives. The spinner becomes a progress feed. The blank wait becomes visible work.
Perceived Performance Is Real Performance
This isn't just a UX trick. It's a more honest architecture.
When you batch everything into one terminal response, you're hiding the shape of your system from the user. They have no idea if something is processing or stuck. Streaming exposes the progress in a way that builds trust rather than eroding it.
There's also a practical reliability argument. Long-held HTTP responses in serverless environments can hit timeout limits that chunked streaming avoids by keeping the connection active. You're not just improving UX — you're working with the infrastructure rather than against it.
Teams building production streaming pipelines often hit a secondary problem once they've solved the transport layer: managing the data backbone reliably under load. That's where infrastructure like Turboline comes in — handling the streaming delivery layer so your team can stay focused on what actually shows up in the UI, not on reinventing transport reliability from scratch.
The Concrete Shift
Stop designing your API responses around "when is everything ready" and start designing them around "what can I say right now." That reframe changes the implementation, the UX, and the mental model you bring to long-running operations.
If a request takes 15 seconds and users see nothing until the end, you have a streaming problem, not a speed problem. The fix was always available — most teams just never reached for it.
Top comments (0)