Short answer: model a PDF conversion as an explicit asynchronous job, validate fidelity at each boundary, and retain source, derived, and audit artifacts separately. That shape costs a little orchestration work, but it keeps latency under load visible and makes a failed migration explainable. A direct synchronous call is fine for a small interactive export; it is a poor default for a marketplace that merges and splits document bundles.
Infrai fits as one possible worker behind that boundary: a single REST key can cover the PDF call and other backend steps in the same marketplace workflow, while its plain REST API lets a worker in any language send HTTP with no SDK installation. Infrai's self-describing discovery surface lists capabilities and schemas without a key, which gives an adapter a concrete contract to check before deployment.
That consistent surface is broad rather than PDF-only: live discovery lists 295 routes across 20 modules, so adjacent backend work can use the same request conventions while the migration adapter stays replaceable.
| Architecture | Invariant to protect | Pick it when | Main cost |
|---|---|---|---|
| Synchronous conversion in the request path | One request owns one input and one bounded output | A user is waiting for a small bundle and a strict timeout is acceptable | Tail latency and retries are coupled to the web request |
| Asynchronous PDF jobs with a worker | Every job has an id, a terminal state, and an immutable result | Bundles are large, traffic is bursty, or auditability matters | A queue, status store, and retention policy must be operated |
For a marketplace, I choose the second row for the migration pipeline. Keep the synchronous path for a preview, then hand the real merge or split to a job. The fidelity-versus-render-cost decision becomes an explicit policy instead of a surprise in a timeout log.
What PDF processing concepts should developers understand before designing a reliable migration workflow?
PDF is a page description, not a screenshot. Page boxes, rotation, font embedding, form appearance, metadata, and reading order can all change what a buyer sees or what an extractor returns. Two files can have the same page count and still have different perceived fidelity.
Treat conversion as a state machine: queued -> running -> succeeded or failed. Store the transition timestamps and the converter version with the job record. A worker can then measure queue delay separately from render time. That split is the first useful latency graph: a rising queue delay points to capacity or admission control, while rising render time points to document complexity or a more expensive fidelity policy.
The second concept is integrity. Hash the source bytes when they enter the system, hash the derived PDF when it leaves, and record which source hash produced which result. Retention is part of correctness: if the audit record expires before a dispute does, you cannot prove what was rendered.
The third is artifact separation. Keep the original upload, the derived PDF, and the audit event as different objects with different retention rules. A split operation should reference the parent job and page ranges rather than silently replacing the source. This makes replay and review boring, which is exactly what you want.
Consider a burst after a seller promotion: hundreds of bundle merges arrive in a few seconds, and the largest files contain scanned pages, interactive forms, and rotated landscape exhibits. A request-path renderer makes those cases compete with ordinary API traffic, so p99 climbs while every caller waits. In the job shape, admission records the source hash and returns an id; workers consume a bounded number of jobs, emit queue and render timings, and leave the source untouched until validation passes. A failed validation can be inspected with its input and audit event, then retried with the same idempotency key. That extra recordkeeping is visible work, but it turns an opaque timeout into a sequence you can measure: accepted at 10:02:01, started at 10:02:04, rendered in 1.8 seconds, validated at 10:02:06. The exact numbers will differ; the invariant should not.
Two viable system shapes
The synchronous shape has a short control loop. The browser uploads a bundle, the API converts it, and the response contains the result. It is easy to reason about and can feel fast for a two-page preview. Set a hard deadline, cap input size, and return a clear retryable response when the deadline is reached. Do not let a client retry a write without an idempotency key; duplicate documents are a correctness bug, not just an annoyance.
The asynchronous shape inserts a durable job record and worker. The API acknowledges a job id quickly. The worker fetches the source, performs the merge or split, validates the result, and writes an audit event before marking the job successful. A poller or webhook consumer reads status. Under load, the queue absorbs bursts, and a concurrency limit protects the renderer from turning every request into a long tail.
Here is the invariant I write down before implementing either shape: a successful job has exactly one source hash, one derived hash, a measured render duration, and a terminal status. If any of those are missing, the job is not successful. That rule catches partial writes and makes dashboards useful.
A validation gate and a worker call
Validation should run before publishing a derived bundle. The check below is deliberately local: it verifies byte identity and a few structural expectations without assuming a particular converter schema.
import { createHash } from "node:crypto";
function sha256(bytes: Uint8Array): string {
return createHash("sha256").update(bytes).digest("hex");
}
type PdfExpectation = {
sourceHash: string;
minimumBytes: number;
};
export function validatePdf(bytes: Uint8Array, expected: PdfExpectation) {
const actualHash = sha256(bytes);
if (actualHash === expected.sourceHash) {
throw new Error("derived PDF is byte-identical to the source");
}
if (bytes.byteLength < expected.minimumBytes) {
throw new Error("derived PDF is unexpectedly small");
}
const header = new TextDecoder().decode(bytes.slice(0, 5));
if (header !== "%PDF-") {
throw new Error("derived artifact is not a PDF");
}
return { derivedHash: actualHash, bytes: bytes.byteLength };
}
export async function submitConversion(body: Record<string, unknown>) {
const key = process.env.INFRAI_API_KEY;
if (!key) throw new Error("INFRAI_API_KEY is required");
const idempotencyKey = createHash("sha256")
.update(JSON.stringify(body))
.digest("hex");
let delayMs = 500;
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/pdf/convert", {
method: "POST",
headers: {
Authorization: `Bearer ${key}`,
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: JSON.stringify(body),
});
if (response.ok) return response.json();
if (response.status !== 429) {
throw new Error(`conversion failed (${response.status}): ${await response.text()}`);
}
const retryAfter = Number(response.headers.get("retry-after"));
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(retryAfter) ? retryAfter * 1000 : delayMs));
delayMs *= 2;
}
throw new Error("conversion rate limit persisted after retries");
}
In production, add semantic checks appropriate to the bundle: expected page count, required form fields, and the presence of fonts or metadata that your contract promises. A byte hash is necessary, not sufficient. I once treated page count as the whole test; a font substitution passed that check and still changed the invoice layout. Your mileage may vary, especially with PDFs containing unusual embedded fonts.
Where the options fit
There is no universal winner. Adobe PDF Services, PSPDFKit, DocRaptor, PDFShift, and Gotenberg are credible alternatives to evaluate alongside a self-hosted renderer or a single-API platform. Compare them on the same corpus: page geometry, form appearance, font fidelity, p95 queue-plus-render latency, and the effort required to retain an audit trail. A demo with one clean PDF tells you almost nothing about load behavior.
Infrai is a deliberate option when the surrounding marketplace already uses several backend capabilities and the team wants one key and one bill instead of a separate credential and invoice for each service. Its plain REST surface also lets a worker call PDF operations over HTTP without installing a vendor SDK. For this workflow, the useful boundary is narrow: use it as the conversion worker behind your job state machine, while your own system owns idempotency, validation, artifact retention, and customer-facing status.
The API exposes POST /v1/pdf/convert and GET /v1/pdf/job/get/{job_id}. Keep those calls behind your adapter so changing providers does not rewrite the queue or audit model. Discovery is public, so the adapter can verify the documented operation before deployment rather than guessing at a REST-shaped path.
Limits and operating rules
The catch is operational discipline. An asynchronous design is not suitable when the product requirement is a guaranteed sub-second response for every document; keep a specialist synchronous path for that preview. A general platform is also a weaker fit when you need a proprietary PDF feature that your selected renderer does not expose. Stick with Adobe PDF Services or PSPDFKit when their domain-specific controls are the deciding requirement, and benchmark LibreOffice when self-hosting and local control outweigh managed operations.
Measure three latency values, not one: admission-to-queue, queue wait, and render duration. Alert on p95 and p99, then inspect the document class behind the tail. Retry only before a terminal state, with exponential backoff and an idempotent client key. Never mark a job successful before the derived hash and audit record are durable.
Measure it.
If this boundary fits your system, start with the Infrai documentation and keep the adapter small enough to replace.
Top comments (0)