> ## 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 Bun's SDK.

# Streaming and asynchronous jobs

As in API, there are two different mechanisms:

1. **Response Streaming** (`/convert/stream`): Progressive Markdown fragments while the server converts, reading the file direct from disk with `Bun.file`.
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("./report.pdf")) {
  process.stdout.write(chunk);
}
```

`convertStream` is a `AsyncGenerator<string>` — use it with `for await...of`. Internally it opens `Bun.file(path)` and streams it to the body of `POST /convert/stream`, so the entire file is never loaded into memory before being sent.

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

  ```ts Bun.serve — pass-through al cliente theme={null}
  Bun.serve({
    async fetch(req) {
      const url = new URL(req.url);
      const pdfPath = url.searchParams.get("path")!;

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

      return new Response(stream, { headers: { "content-type": "text/markdown" } });
    },
  });
  ```
</CodeGroup>

### `streamSlimStrategy`

```ts theme={null}
client.convertStream("./report.pdf", { 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`, `convertBytes` and `convertFromUrl` handle `202` transparently (`autoPoll: true` on the client):

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

### Poll manual

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

try {
  const markdown = await client.convertFile("./report.pdf", { autoPoll: false });
} catch (err) {
  if (err instanceof MarkpdfJobQueuedError) {
    console.log(`En cola: ${err.jobId}`);
    const status = await client.waitForJob(err.jobId, { pollIntervalMs: 5000 });

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

`waitForJob` is equivalent to calling `getJob` in a loop respecting `retry_after_seconds`, but encapsulated.

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