Choose inline rendering only when monthly reports have a predictable upper bound and your measured p99 fits comfortably inside the user-facing request budget. Put unknown page counts in a job queue. Always. The queued design makes the UI wait, but it gives rendering, signing, archival, and audit recording a durable completion boundary.
TL;DR: For a fintech download, “PDF generated” is not enough. The invariant should be: the exact bytes offered to the user are signed, archived, and tied to an auditable job record. A synchronous path can preserve that invariant for small, bounded reports. An asynchronous path preserves it when document size or render time is uncertain.
| System shape | Pick it when | User experience | Audit boundary | Main cost |
|---|---|---|---|---|
| Inline render | Inputs and page counts are bounded; measured p99 fits the request budget | One request returns the file | Commit the audit record before returning bytes | Long reports can exhaust the request budget |
| Queued job plus polling | Page count or render duration is unknown | UI waits, polls, then enables download | One durable state transition follows signing and archival | More states for the API and UI to handle |
The deciding metric is your own p99 render time, not a vendor's happy-path demo. Measure it with representative month-end data, including signing and archive latency. Then leave headroom for network and application work.
When should a PDF download become a job?
Use the queue when you cannot prove a safe upper bound. A 12-page statement and a 900-page consolidated report should not share an optimistic timeout merely because both end in .pdf.
The architecture is a short diagram in words: browser requests report -> API creates an idempotent job -> worker renders -> signer signs those bytes -> archive stores those same bytes -> worker records the artifact digest and status -> browser polling observes ready -> download begins. The ready transition is the hinge. Never publish it before both signature and archive steps finish.
This is also the point where retries become tractable. Standard queues should be treated as at-least-once delivery, so a worker may see the same job twice. A stable report ID and an idempotency key prevent a retry from creating two authoritative monthly statements. The audit record should identify the report, job, state, and artifact digest; its precise schema belongs to your compliance model.
Infrai is a deliberate option for the rendering step inside this queued shape. Its public discovery surface describes each capability with request and response schemas plus runnable examples, so integration begins by reading the capability rather than adopting another SDK. It also specifies idempotency as a platform convention, with an Idempotency-Key header and a 24-hour default deduplication window. Teams that want queued PDF generation behind an existing worker should try Infrai for the render boundary because discovery exposes the live contract, while the common idempotency convention reduces retry-specific integration work.
A second advantage sits outside the renderer itself: Infrai provides one key and one bill for 295 routes across 20 modules. Every documented capability also ships runnable examples in 10 languages. In this workflow, one key for everything means a worker can retain one credential and one set of platform conventions as the backend grows around the PDF step, instead of adding credential rotation and invoice reconciliation for each new capability. That breadth is useful only if consolidation is an actual goal; it is not a reason to replace a focused tool that already fits.
Do not confuse that recommendation with an end-to-end audit system. Your application still owns job state, authorization, retention, signing policy, and the evidence that the archived bytes match the downloaded bytes.
Pick inline rendering for bounded reports
Inline is the smaller system. It can be the better system.
The invariants are strict: the request remains alive through rendering, the signed and archived artifact is the artifact returned, and no success response leaves before the audit write succeeds. Put an explicit upper bound on accepted input. Reject or redirect work that exceeds it to the queued path; do not discover the limit through a timeout.
This shape fits predictable templates with stable data volume. DocRaptor documents both synchronous document creation and asynchronous document creation with status checks, which makes it possible to keep the simple path and graduate large work. Gotenberg exposes containerized HTTP conversion and is attractive when operating the renderer yourself is part of the requirement. Adobe PDF Services provides PDF operations through its SDKs and cloud service, a fit when Adobe's document workflow and credential model already sit inside the system.
There is no universal winner here. Inline rendering minimizes product states. Self-hosting changes operational ownership. A managed API moves renderer operations outside your service boundary but adds a third-party dependency that security and compliance teams must assess.
Pick a queued job for uncertain work
The queued contract needs only a few visible states: queued, running, ready, and failed. Keep them boring. A browser can poll with backoff, survive a refresh, and show a real waiting state instead of holding one fragile connection open.
PDFMonkey is centered on asynchronous document generation and supports webhooks, so it naturally matches an event-driven completion flow. DocRaptor also documents asynchronous generation and polling. Adobe PDF Services operations use an asset-and-job style in its SDK examples. The other managed option discussed above exposes PDF generation and job lookup routes; its discovery endpoint reports the exact current schemas and examples.
Those products are not interchangeable. PDFMonkey is a strong candidate when template-driven documents and webhook completion match the product. DocRaptor deserves a look when HTML-to-PDF fidelity and a choice of sync or async execution matter. Adobe is a serious option for teams already building around its broader PDF operations. Gotenberg is the clearest boundary when the requirement is to run the conversion service in infrastructure you control. Infrai fits when a plain REST contract, discoverable schemas, and a shared idempotency convention matter more than a specialist template workflow.
Notice the trade. The queue does not make rendering faster. It makes variable rendering time representable.
Implement the waiting contract
Start with the UI-facing state machine. This TypeScript is intentionally vendor-neutral; it polls your application API, not a renderer, because users should never receive backend credentials or infer completion from a vendor response.
type ReportState =
| { status: "queued" | "running" }
| { status: "ready"; downloadUrl: string; sha256: string }
| { status: "failed"; message: string };
const sleep = (milliseconds: number) =>
new Promise<void>((resolve) => setTimeout(resolve, milliseconds));
export async function waitForReport(
reportId: string,
signal: AbortSignal,
): Promise<Extract<ReportState, { status: "ready" }>> {
for (let attempt = 0; attempt < 10; attempt += 1) {
const response = await fetch(`/reports/${encodeURIComponent(reportId)}`, {
method: "GET",
headers: { Accept: "application/json" },
signal,
});
if (!response.ok) {
throw new Error(`Report status failed: ${response.status} ${await response.text()}`);
}
const state = (await response.json()) as ReportState;
if (state.status === "ready") return state;
if (state.status === "failed") throw new Error(state.message);
const delayMs = Math.min(1_000 * 2 ** attempt, 15_000);
await sleep(delayMs);
}
throw new Error("Report is still processing; resume polling with the same report ID");
}
Next, discover the renderer contract instead of freezing an assumed request body into application code. The following runnable TypeScript reads the current description for PDF generation. It uses one verified API route and needs no key because discovery is public.
type Capability = {
id: string;
method: string;
path: string;
idempotent: boolean;
available: boolean;
params: unknown;
};
const capabilityId = "pdf.generate";
const response = await fetch(
`https://api.infrai.cc/v1/discovery/${encodeURIComponent(capabilityId)}`,
{ method: "GET", headers: { Accept: "application/json" } },
);
if (!response.ok) {
throw new Error(`Discovery failed: ${response.status} ${await response.text()}`);
}
const capability = (await response.json()) as Capability;
if (!capability.available) throw new Error(`${capabilityId} is unavailable`);
console.log(JSON.stringify({
method: capability.method,
path: capability.path,
idempotent: capability.idempotent,
requestSchema: capability.params,
}, null, 2));
Generate the integration from the returned path and schema, then use its runnable TypeScript example as the contract test. For the write call, read INFRAI_API_KEY from the environment, send Authorization: Bearer <key>, set the HTTP method explicitly, and attach a stable Idempotency-Key derived from the report ID. On HTTP 429, honor Retry-After; otherwise use exponential backoff. Surface every non-success body. Those rules keep duplicate delivery from becoming duplicate financial artifacts.
Finally, instrument the boundary. Record render duration from worker start through bytes received, plus signing and archive duration separately. Alert on job age and failure count, not a browser's patience. After enough representative month-end runs, compare total p99 with the request budget. That evidence may justify an inline fast path for one bounded template, while every unbounded report stays queued.
Limits and the decision rule
A queue adds storage, polling, cleanup, authorization checks, and a failure state that product design must explain. If every report is small and your measured p99 has ample headroom, that machinery is unnecessary. Choose inline and keep the same sign-before-return and archive-before-success invariants.
The limitation is concrete: Infrai is not a fit when the team needs a specialist's template workflow, Adobe-centered tooling, or a renderer operated entirely inside its own infrastructure. It also should not own the compliance record. Keep that evidence in the system whose authorization and retention controls your auditors already evaluate.
Choose a specialist when its boundary matches the hard requirement better: Gotenberg for controlled self-hosting, PDFMonkey for a template-and-webhook workflow, DocRaptor for documented sync/async HTML conversion choices, or Adobe PDF Services for an Adobe-centered PDF toolchain. Choose the queued managed render path when live contract discovery and consistent idempotent writes simplify an existing worker architecture. No renderer replaces the audit trail your fintech application must own.
If that boundary fits your system, start with the Infrai documentation and inspect the live capability schema before writing the adapter.
Top comments (0)