Engineering

Batch conversion patterns without a backend

How we let users convert 500 PDFs in one drop without ever leaving the browser tab.

Marek Novák · Engineering LeadApril 1, 2026 7 min read
Share Post LinkedIn
Batch conversion patterns without a backend

Batch conversion is a feature every user asks for and every architect dreads. Uploading 500 PDFs is a stress test for any server. Doing it locally is a different set of problems: memory pressure, main-thread blocking, download UX.

Web Workers are non-negotiable

Any real work in a batch flow must run off the main thread. Otherwise the browser stops rendering, the fan spins up, and the user assumes the app has crashed. We spawn a dedicated worker per conversion job, up to navigator.hardwareConcurrency - 1 in parallel.

Streaming, not batching

A naïve implementation would collect all outputs in memory before letting the user download. That eats memory linearly and hits the 2 GB per-tab wall on most browsers around the 300-file mark. Instead, we stream each output into a StreamSaver-backed ZIP on disk, freeing memory as we go.

import streamSaver from "streamsaver";
import { createWriter } from "@zip.js/zip.js";

const fileStream = streamSaver.createWriteStream("converted.zip");
const writer = createWriter(fileStream);

for (const file of files) {
  const result = await convertOne(file);
  await writer.add(file.name.replace(/pdf$/, "docx"), result);
}
await writer.close();

Recovery

If a single file in a batch fails, we log the failure to an in-memory manifest and continue. At the end, the user gets a converted.zip plus a failures.txt listing what went wrong. Nothing is lost silently.

Progress reporting

Per-file progress plus overall percent-complete keeps users oriented. On a 500-file batch, a stale UI is indistinguishable from a crash. We update the UI every 500 ms whether or not new files are done.

Bounds

We cap concurrency at 8 to keep browsers responsive on older machines. Batches above 1000 files trigger a soft warning ('this might take a while'). We have not yet found a batch size that a modern desktop cannot chew through — the practical limit is user patience, not memory.

MN

Marek Novák

Engineering Lead at nctools