Problema
Bufferizar resposta inteira do LLM antes de mostrar na UI aumenta TTFB percebido. WebSocket aberto “porque streaming” sem controle de reconexão vira leak e custo.
Solução
SSE para unidirecional servidor→cliente atrás de HTTP/2 com reconexão simples. WebSocket quando há canal bidirecional contínuo (voz, cancelamento fino, múltiplos eventos multiplexados). Sempre definir formato de frame (NDJSON, SSE data:) e limites.
Arquitetura
Cliente: ReadableStream / EventSource
Servidor Node: transform stream do SDK OpenAI → SSE
Infra: proxies com timeout adequado (nginx `proxy_read_timeout`)
- Cancelamento: abort no fetch do cliente deve propagar ao upstream do modelo.
Código
// handler SSE simplificado
export async function GET(req: Request) {
const { signal } = req;
const stream = new ReadableStream({
async start(controller) {
for await (const token of llmTokens({ signal })) {
controller.enqueue(encoder.encode(`data: ${token}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
headers: {
"Content-Type": "text/event-stream",
Connection: "keep-alive",
"Cache-Control": "no-cache",
},
});
}Performance
Evitar stringify gigante por chunk; comprimir apenas se intermediário suportar; pooling de conexões no servidor outbound.
Melhorias futuras
Resumable streams; métricas p95 por provedor; circuit breaker se upstream falhar.
Conclusão
Streaming bem feito é UX + rede + cancelamento. Poucos artigos ligam os três — use isso como diferencial.