Short answer: choose a hosted PDF API when a rental-application workflow needs predictable document output across workers and regions; choose a local library when you can own font files, rendering capacity, and the failure queue. Under load, the deciding metric is queue-to-download latency, not the fastest single render.
The constraint that changed the choice
Rental applications look like a PDF problem until support tickets arrive. A scanned pay stub needs searchable text, an application needs a signed packet, and every artifact must be reproducible months later. Template ownership becomes the real decision: do templates live in your repository, or in a service whose rendering runtime you do not control?
I keep the input contract boring. HTML or a small data object enters a worker; a PDF blob leaves it. The Blob interface is deliberately opaque, which makes it useful for passing bytes between browser, queue, and object storage without pretending the document is a string.
The hosted route buys operational reach. A local route buys control. Neither removes the need for back-pressure, retention rules, or audit logs.
Measure twice.
When is a hosted PDF API preferable to local libraries under load?
Measure the whole path: enqueue, wait, render, upload, and fetch. A vendor dashboard that reports render time can miss a saturated client connection or a cold worker. Set a separate budget for each stage and record p50, p95, and p99 by document size. A five-second p99 may be fine for an overnight export and unacceptable while an applicant waits on a support call.
The failure mode I watch is queue inflation. At 200 concurrent applications, a renderer that is quick in isolation can spend most of its time waiting for CPU, fonts, or a browser process. Hosted capacity can absorb bursts, but network round trips and rate limits become part of your SLO. Local workers avoid that hop, yet they need autoscaling, patching, and a deliberate memory ceiling.
I once treated a 40-page packet as “just another request.” That was wrong. The packet pulled image decoding and font embedding into the hot path, and retries multiplied the work. The fix was an idempotency key plus a size-aware queue, not a new template engine. I also moved image normalization out of the renderer, capped each job's byte budget, and recorded the page count before acknowledging the queue message. That extra bookkeeping made the slow cases visible instead of letting them look like random network noise. Your mileage may vary when scans come from older devices; measure with your own corpus.
For the smallest working implementation in Node.js, keep the renderer replaceable. The rest of the application only knows about bytes and metadata.
type PdfInput = {
applicationId: string;
html: string;
idempotencyKey: string;
};
type PdfOutput = {
bytes: Uint8Array;
contentType: "application/pdf";
};
interface PdfRenderer {
render(input: PdfInput): Promise<PdfOutput>;
}
async function createPacket(
renderer: PdfRenderer,
input: PdfInput,
): Promise<PdfOutput> {
if (!input.html.trim()) throw new Error("empty-template");
return renderer.render(input);
}
For a hosted implementation, render is an HTTP call with a timeout, retry policy, and idempotency key. For a local implementation, it can invoke a library in the same worker. Keep the timeout shorter than the queue visibility window, and persist the input hash beside the resulting object so a retry cannot silently produce a different packet.
Trade-offs that survive production
| Concern | Hosted API | Local library |
|---|---|---|
| Template ownership | Central service; review its editor and versioning model | Git, code review, and pinned assets are yours |
| Latency under bursts | Network and provider queue add variance; regional placement matters | No network hop, but CPU, memory, and process pools are your problem |
| Data boundary | Uploaded scans require retention and deletion guarantees | Data can stay inside your account, subject to your own storage controls |
| Reproducibility | Pin template and renderer versions if the service allows it | Pin library, fonts, and OS image together |
| Failure handling | Respect quotas and retry semantics | Build health checks, crash recovery, and capacity controls |
The catch is ownership. A hosted API is not suitable when legal policy forbids sending applicant scans outside your controlled boundary, or when you need pixel-level changes without waiting for a service release. Stick with a local library in those cases. Conversely, local rendering is a poor fit when your team cannot operate browser processes, fonts, and regional capacity; a hosted service may be the calmer boundary.
Cost is a secondary check. Compare total worker time, storage, egress, on-call effort, and the price of failed retries. A low per-call quote does not compensate for a queue that misses the applicant-facing SLO.
First, split OCR from packet assembly. OCR produces searchable text and confidence metadata; assembly consumes a versioned manifest. That lets support staff re-run a page without rebuilding every signature and attachment.
Next, run a load test with realistic scans, not empty fixtures. Track queue age, render duration, byte size, and retry count. Alert on age and p99, then sample finished PDFs for missing glyphs and page-count drift. Keep the source hash, template version, and renderer version with each object.
I am not sure one universal threshold exists. A property manager with ten applications per hour has a different knee than a national intake queue. The durable rule is simpler: own the template and runtime when control is the requirement; outsource rendering when operating that runtime is the bottleneck.
References and Further reading
- MDN, Blob API: https://developer.mozilla.org/en-US/docs/Web/API/Blob
Top comments (0)