> ## 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

> Consume /convert/stream and handle 202 (auto-async) with Node.js SDK.

# Streaming and asynchronous jobs

As in API, there are two different mechanisms:

1. **Response Streaming** (`/convert/stream`, `/convert/stream-from-url`): Progressive Markdown fragments while the server converts.
2. **Jobs due to saturation** (`202`): when all backends are busy, the request is queued and responded to `202` with a `job_id`. See [`GET /jobs/{id}`](/docs/api/jobs).

## Streaming with `convertStream`

```ts theme={null}
const client = new MarkpdfClient({ apiKey: "YOUR_API_KEY" });

for await (const chunk of client.convertStream({ data: buffer }, { filename: "report.pdf" })) {
  process.stdout.write(chunk);
}
```

`convertStream` returns a `AsyncIterable<string>` — use it with `for await...of` in Node/Deno/Bun, or consume the iterator manually.

<CodeGroup>
  ```ts Local file theme={null}
  for await (const chunk of client.convertStream({ data: buffer }, { filename: "report.pdf", slim: true })) {
    handle(chunk);
  }
  ```

  ```ts Desde URL theme={null}
  for await (const chunk of client.convertStream({ url: "https://bucket.example.com/report.pdf?sig=..." }, { slim: true })) {
    handle(chunk);
  }
  ```

  ```ts Route Handler (App Router) — pass-through al cliente theme={null}
  export async function GET(req: Request) {
    const stream = new ReadableStream({
      async start(controller) {
        const encoder = new TextEncoder();
        for await (const chunk of client.convertStream({ url: pdfUrl })) {
          controller.enqueue(encoder.encode(chunk));
        }
        controller.close();
      },
    });
    return new Response(stream, { headers: { "content-type": "text/markdown" } });
  }
  ```
</CodeGroup>

### `streamSlimStrategy`

```ts theme={null}
client.convertStream({ data: buffer }, { slim: true, streamSlimStrategy: "sampled" });
```

* `"off"`: pure streaming per page, without noise detection. TTFB minimum.
* `"sampled"` (default): Sample the first few pages, then output per page.
* `"full"`: materializes the entire document before issuing. Better cleaning, worse TTFB.

See [Parameters](/docs/api/parameters#streaming-only-parameters).

<Note />

## Jobs due to saturation (202)

By default, `convertFile` and `convertFromUrl` handle `202` transparently (`autoPoll: true`):

```ts theme={null}
// SDK waits and polls automatically
const markdown = await client.convertFile(buffer, { filename: "report.pdf" });
```

### Poll manual

```ts theme={null}
import { MarkpdfJobQueuedError } from "@markpdf/sdk";

try {
  const markdown = await client.convertFile(buffer, {
    filename: "report.pdf",
    autoPoll: false,
  });
} catch (err) {
  if (err instanceof MarkpdfJobQueuedError) {
    console.log(`En cola: ${err.jobId}`);
    let status;
    do {
      await new Promise((r) => setTimeout(r, err.retryAfterSeconds * 1000));
      status = await client.getJob(err.jobId);
    } while (status.status === "queued" || status.status === "processing");

    if (status.status === "completed") {
      const markdown = status.body as string;
    } else {
      throw new Error(status.error);
    }
  } else {
    throw err;
  }
}
```

<Tip>
  Under normal load conditions you won't see `202` — it only happens when all backends are saturated. `autoPoll: true` (the default) already covers it without additional code in most cases.
</Tip>

<Warning>
  Results for completed jobs expire in approximately 1 hour. If you save a `jobId` and receive `404` when querying it, resend the original conversion.
</Warning>
