Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
API Endpoints  ›  Lesson

Streaming Responses

Streaming 13 minA route handler can return a Response whose body is a stream, sending data in chunks as it becomes ready instead of waiting for the whole result.

Streaming Responses

Trailhead's park rangers can export every hiking log as a CSV: 20,000 rows that take about ten seconds to gather. Built the usual way, the download sits at "waiting" for ten seconds, then everything arrives at once, and the server holds the whole file in memory meanwhile. Streaming lets the first rows leave while the rest are still being gathered.

The rule

Don't wait for the whole answer: return a stream and send it piece by piece.

A Response body does not have to be a finished string. It can be a ReadableStream: a source you push chunks into over time. Each chunk goes to the client as soon as you push it.

// app/api/export/route.ts
import { getLogBatches } from "@/lib/logs";

export async function GET() {
  const encoder = new TextEncoder();

  const stream = new ReadableStream({
    async start(controller) {
      controller.enqueue(encoder.encode("date,trail,hiker,minutes\n"));
      for await (const batch of getLogBatches(500)) {
        const rows = batch.map((l) => `${l.date},${l.trail},${l.hiker},${l.minutes}`);
        controller.enqueue(encoder.encode(rows.join("\n") + "\n"));
      }
      controller.close();
    },
  });

  return new Response(stream, {
    headers: { "Content-Type": "text/csv; charset=utf-8" },
  });
}

Line by line:

  • TextEncoder turns text into bytes, which is what a stream carries.
  • enqueue pushes one chunk to the client.
  • close says the response is complete. Forget it and the client keeps waiting.

On the receiving side, a browser download simply grows as chunks arrive. Code can read chunks as they come with response.body.getReader().

Where streaming shines:

  • Large exports: the download starts at once, and the server never holds the whole file.
  • Slow, incremental results, such as text from an AI model, shown word by word.
  • Progress updates for a long job.

Where it does not help: small, quick responses. Response.json(...) is simpler and just as fast.

The shortcut

Ask "Can part of the answer be useful before the rest exists?" Yes: stream it and enqueue each part when ready. No: build it and send it whole.

Streaming the ranger exportStep 1 of 4
  1. Can part be useful early?

    Yes. The first 500 rows are valid CSV on their own.

The trap is collecting every row into an array and only then enqueueing it as one chunk. That is a stream in name only: the client still waits for everything.

Drills

Answer fast — trust the rule.

quick drill

What can a route handler pass as a Response body to stream data?

quick drill

Inside the stream's start function, which call sends one chunk?

quick drill

What happens if you never call controller.close()?

quick drill

Which Trailhead endpoint gains least from streaming?

Remember

  • Don't wait for the whole answer: return a ReadableStream and enqueue each piece.
  • Shortcut: "can part of it be useful before the rest exists?" Then stream.
  • Trap: building everything first and enqueueing once, or forgetting close().

Practice — level up

0/1 solved

Write your answer, then compare it with a rubric and a model answer.

ExplainSend it as you go

Trailhead's /api/export builds a CSV of 20,000 trail logs, which takes ten seconds. Explain how streaming the response changes the experience and outline how the handler produces the stream.