> ## Documentation Index
> Fetch the complete documentation index at: https://docs.markpdf.tech/llms.txt
> Use this file to discover all available pages before exploring further.

# Streaming and asynchronous jobs

> Streaming in Route Handlers and handling of 202 (auto-async) in Server Actions.

# Streaming and asynchronous jobs

## Streaming to the browser

A Route Handler can forward the streaming API directly as `ReadableStream`, without buffering the entire document in server memory:

```ts app/api/convert/stream/route.ts theme={null}
import { getServerClient } from "@markpdf/nextjs";

export const runtime = "nodejs";

export async function POST(req: Request) {
  const client = getServerClient({ apiKey: process.env.MARKPDF_API_KEY! });
  const { url } = await req.json();

  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        for await (const chunk of client.convertStream({ url })) {
          controller.enqueue(encoder.encode(chunk));
        }
      } finally {
        controller.close();
      }
    },
  });

  return new Response(stream, { headers: { "content-type": "text/markdown; charset=utf-8" } });
}
```

On the client, consume the response with `ReadableStream` native or a streaming library of your choice:

```tsx theme={null}
async function streamConversion(url: string) {
  const res = await fetch("/api/convert/stream", {
    method: "POST",
    body: JSON.stringify({ url }),
    headers: { "content-type": "application/json" },
  });
  const reader = res.body!.getReader();
  const decoder = new TextDecoder();
  let full = "";
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    full += decoder.decode(value, { stream: true });
  }
  return full;
}
```

<Note>
  Server Actions do not support response streaming (they are serialized RPC calls, not raw `Response` calls). For actual streaming to the client, use a Route Handler.
</Note>

## Jobs due to saturation (202) in Server Actions

The `convertFormData` and `convertUrlAction` helpers inherit `autoPoll` from SDK of Node.js (enabled by default): if the server responds `202`, the Server Action waits internally for the poll and returns the final Markdown once completed.

```ts app/actions.ts theme={null}
"use server";

import { convertFormData } from "@markpdf/nextjs";

export async function convertUpload(formData: FormData) {
  // If there were 202, this call takes longer — the Server Action blocks until "completed"
  return convertFormData(formData, {
    apiKey: process.env.MARKPDF_API_KEY!,
    mode: "fast",
  });
}
```

<Warning>
  Server Actions have their own platform timeout (varies depending on your hosting provider). If you expect conversions that may take several minutes due to saturation, consider a Route Handler with manual polling from the client instead of blocking the Server Action.
</Warning>

### Manual poll from the client

```ts app/api/jobs/[id]/route.ts theme={null}
import { getServerClient } from "@markpdf/nextjs";
import { NextResponse } from "next/server";

export async function GET(_req: Request, { params }: { params: { id: string } }) {
  const client = getServerClient({ apiKey: process.env.MARKPDF_API_KEY! });
  const status = await client.getJob(params.id);
  return NextResponse.json(status);
}
```

```tsx theme={null}
async function pollUntilDone(jobId: string): Promise<string> {
  while (true) {
    const res = await fetch(`/api/jobs/${jobId}`);
    const status = await res.json();
    if (status.status === "completed") return status.body;
    if (status.status === "failed") throw new Error(status.error);
    await new Promise((r) => setTimeout(r, 5000));
  }
}
```

<Tip>
  Under normal load conditions you won't see `202` — it only happens when all backends are saturated. The automatic poll of `convertFormData`/`convertUrlAction` already covers it for most cases.
</Tip>
