Streaming AI From the Edge: SSE Token Streaming with Hono, @ai-sdk and Next.js

7 min readYaseen Khatib · AI Architect

A support answer that arrives all at once, after a three-second pause, feels broken even when it is correct. The same answer streamed word by word feels instant. Perceived latency is the product, and streaming is how you win it. The tricky part is that most tutorials show you half the wire — the React hook, or the backend handler — and leave you to guess how the two actually connect. This is the whole path: model to Hono to Next.js, end to end.

Server-Sent Events, not WebSockets

LLM streaming is one-directional: the server emits tokens, the client listens. That is exactly the shape SSE was built for, and it is far simpler than a WebSocket — plain HTTP, no upgrade handshake, automatic reconnection, and a perfect fit for the request/response model of a Cloudflare Worker. Reaching for a bidirectional socket here is solving a problem you do not have.

modelHono · SSEedge workerNext.js · useChatprogressive paintfirst token < 1s
No spinner. Tokens leave the model and reach the user's screen as a continuous stream across the edge.

The backend: Hono streams the model through

On the Worker, the job is to take the model's token stream and relay it to the client without buffering. The @ai-sdk gives you a streaming result whose body is already a readable stream of tokens; Hono hands that straight back as the response. The Worker is a pass-through pipe with grounding attached — it never waits for the full answer before sending the first byte.

worker.ts
// Hono returns the model's token stream directly — no buffering
app.post("/ask", async (c) => {
  const { query } = await c.req.json();
  const ctx = await retrieve(query, c.env);

  const result = streamText({
    model: gemini(c.env),
    system: GROUNDING_CONTRACT,
    messages: [{ role: "user", content: withContext(query, ctx) }],
  });

  return result.toDataStreamResponse();  // SSE, straight to the edge
});

The frontend: App Router consumes it

On the Next.js side, the useChat hook from the AI SDK consumes that same stream and re-renders as each token lands. Point it at the Worker route, render the streaming message, and the progressive paint is handled for you. Because the App Router serves a static, prerendered shell, the page is interactive before the first token is even requested — the heavy lifting is all on the stream, not the bundle.

chat.tsx
"use client";
import { useChat } from "@ai-sdk/react";

export function SupportChat() {
  const { messages, input, handleInputChange, handleSubmit } =
    useChat({ api: "https://agent.example.workers.dev/ask" });
  // each token re-renders the last message — progressive paint, free
  return /* form + message list */;
}

Why the edge makes it feel instant

Streaming hides total latency, but the first token still has to travel. Running the Worker at the edge means that first token has the shortest possible trip to the user, so the answer starts almost immediately — and once it starts, the brain stops counting. The combination of edge proximity and token streaming is what turns "technically fast" into "feels instant."

Users do not experience total latency; they experience time-to-first-token. Stream from the edge and the wait disappears even when the work does not.

This is the delivery layer for the edge-native RAG pipeline, and it carries grounded answers — the stream is fast, but it is still constrained to the retrieved context.

Looking to architect a similar system?

Let's ship it at AI-speed.

Start a conversation →