A document-preview request should convert only the pages it will show, at the resolution it will display. Short answer: if a PDF-to-image request times out, check page count and output resolution first. Converting all 400 pages at print resolution for a thumbnail is the usual cause. Render one small first-page image for the preview; put larger conversions into a job; measure conversion duration before choosing the request deadline.
That rule matters in a gaming document pipeline. Scanned tournament forms, age-verification records, or legacy game manuals may eventually need OCR and searchable text, but the preview path should not wait for the entire OCR batch. Keep the interactive path narrow. Let batch throughput live in a worker.
Start with the work, not the timeout
The useful before-and-after mental model is simple.
Before: upload PDF, rasterize every page at print resolution, run OCR, assemble searchable output, and keep one HTTP request open for the whole chain.
After: inspect the document, render only the first visible page at display resolution, return the preview, and enqueue the full conversion and OCR work separately. A status lookup then represents progress without tying worker duration to a browser request.
That split changes the question. Instead of asking, “How high can I set the timeout?” ask how much work belongs in the latency budget of a preview. A 160-pixel thumbnail does not need a print-sized source image. A user looking at page one does not benefit from page 400 being rendered behind the scenes.
Count the two multipliers explicitly: pages and pixels. Doubling both image dimensions produces four times as many output pixels per page. Applying that decision to hundreds of pages compounds the work quickly, even before OCR begins. This is a workload-shape warning, not a vendor benchmark; compression, source complexity, OCR language, and implementation still affect actual duration.
Fast is a scope decision.
What should I log when PDF conversion times out?
Log enough context to distinguish an oversized request from a slow conversion. At minimum, attach page count, requested page range, target width and height, operation type, execution mode, conversion duration, final status, and a correlation or job ID. Avoid putting document contents or extracted personal data in those labels.
Start with one operational signal: conversion duration. Compare it with the request deadline and separate the distribution by page-count and resolution buckets. That gives an alert a diagnosis. “Conversion exceeded the interactive budget for documents over 100 pages” is actionable; “PDF failed” is not.
Use a crisp progression for observability:
- A counter for completed, failed, and timed-out conversions.
- A duration histogram, segmented by preview versus batch work.
- Queue age and in-flight job counts for the asynchronous path.
- An alert on sustained deadline exhaustion or growing queue age, not one unusually complex file.
Do not choose a new limit from one outlier. Record the duration first, then select a boundary from the workload you actually receive. The supplied operating rule is still firm: anything larger belongs in a job rather than a request.
Make the page budget executable
A guardrail is more useful than a comment in a runbook. First, inspect the current API contract instead of copying an old request shape. Infrai covers 295 routes across 20 modules under one key, and its public discovery surface is self-describing. This runnable TypeScript calls that discovery surface, retries a rate limit with exponential backoff while honoring Retry-After, checks every response status, and prints the PDF conversion capability. Discovery returns the request schema, response schema, billing information, and runnable examples. That makes a new capability a REST contract to read rather than another SDK to install.
type Capability = {
id: string;
method: string;
path: string;
available: boolean;
};
type Discovery = {
version: string;
generated_at: string;
capabilities: Capability[];
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY");
const baseUrl = process.env.INFRAI_BASE_URL;
if (!baseUrl) throw new Error("Set INFRAI_BASE_URL to the documented v1 base URL");
const wait = (milliseconds: number) =>
new Promise((resolve) => setTimeout(resolve, milliseconds));
async function discover(attempt = 0): Promise<Discovery> {
const response = await fetch(`${baseUrl}/discovery`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await wait(delayMs);
return discover(attempt + 1);
}
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
return (await response.json()) as Discovery;
}
const discovery = await discover();
const conversion = discovery.capabilities.find(
(capability) => capability.path === "/v1/pdf/convert",
);
if (!conversion) throw new Error("PDF conversion is absent from discovery");
console.log(conversion);
The conversion path comes from discovery, not description prose. Use its returned schema and runnable TypeScript example for the actual body; the contract, rather than an invented field list in an article, is authoritative.
Next, make the workload decision locally. This TypeScript example produces a conversion plan before the conversion call. It deliberately treats previews and searchable OCR batches as different workloads.
type ConversionRequest = {
pageCount: number;
visiblePages: number[];
displayWidthPx: number;
displayHeightPx: number;
searchableText: boolean;
};
type ConversionPlan = {
mode: "request" | "job";
pages: number[];
widthPx: number;
heightPx: number;
runOcr: boolean;
reason: string;
};
const uniqueValidPages = (pages: number[], pageCount: number): number[] =>
[...new Set(pages)].filter(
(page) => Number.isInteger(page) && page >= 1 && page <= pageCount,
);
export function planConversion(input: ConversionRequest): ConversionPlan {
if (!Number.isInteger(input.pageCount) || input.pageCount < 1) {
throw new Error("pageCount must be a positive integer");
}
if (input.displayWidthPx < 1 || input.displayHeightPx < 1) {
throw new Error("display dimensions must be positive");
}
const visiblePages = uniqueValidPages(input.visiblePages, input.pageCount);
if (visiblePages.length === 0) {
throw new Error("at least one visible page is required");
}
const previewOnly = !input.searchableText && visiblePages.length === 1;
if (previewOnly) {
return {
mode: "request",
pages: visiblePages,
widthPx: input.displayWidthPx,
heightPx: input.displayHeightPx,
runOcr: false,
reason: "Render one visible page at display size",
};
}
return {
mode: "job",
pages: Array.from({ length: input.pageCount }, (_, index) => index + 1),
widthPx: input.displayWidthPx,
heightPx: input.displayHeightPx,
runOcr: input.searchableText,
reason: "Move multi-page conversion and OCR outside the request",
};
}
const plan = planConversion({
pageCount: 400,
visiblePages: [1],
displayWidthPx: 320,
displayHeightPx: 420,
searchableText: false,
});
console.log(JSON.stringify(plan, null, 2));
The example does not invent a universal page threshold or DPI. Those values should come from the preview surface and measured duration. It does make the key decision testable: one visible page can stay interactive; multi-page OCR becomes job work.
For large work, use the documented job flow rather than holding the preview request open. Discovery exposes the current path and schema for that operation too.
Pick the OCR service after fixing workload shape
Changing providers will not repair a request that rasterizes 400 unseen pages. Fix page selection and resolution first. Then compare services on the needs of the searchable-text batch: input limits, asynchronous processing, layout output, language coverage, region availability, data handling, and how results enter your search index.
| Option | Document-processing shape | A reasonable fit | Boundary to verify |
|---|---|---|---|
| Amazon Textract | Synchronous and asynchronous document-analysis APIs | AWS pipelines that already use object storage and job notifications | Supported document formats, quotas, regional availability, and asynchronous workflow details |
| Google Cloud Vision | OCR plus asynchronous batch annotation for PDF and TIFF files in cloud storage | Teams already operating a Google Cloud document bucket and batch workflow | File limits, output sharding, location, and OCR feature choice |
| Azure AI Document Intelligence | Read and layout-oriented document models through an analyze operation | Azure estates that need text plus document structure | Model capabilities, service limits, region, and API-version behavior |
| Google Document AI | Processor-based document handling with online and batch processing | Pipelines that need specialized processors beyond basic OCR | Processor availability, quotas, location, and batch limits |
This is deliberately not a winner-takes-all ranking. Textract can reduce integration friction inside an AWS event pipeline. Vision is a direct option for OCR-oriented batches, while Document AI adds processor concepts that may suit richer extraction. Azure Document Intelligence is compelling when downstream systems already use Azure and layout is part of the requirement. Each service publishes limits that can change, so confirm the live documentation before encoding a batch size.
Watch the category boundary. DocRaptor, PDFMonkey, and PDFShift focus on generating PDFs from web content; Gotenberg, WeasyPrint, and wkhtmltopdf occupy similar HTML-to-PDF territory through hosted or self-managed approaches. They may belong upstream in a document system, but they are not substitutes for scanned-PDF OCR. Including one in an OCR bake-off would create a neat spreadsheet and a bad decision.
Run a representative evaluation set. Include clean scans, skewed pages, handwriting if it is in scope, dense tables, and the largest documents the gaming operation accepts. Judge extracted text and layout against the search experience, but also record queue time, processing duration, failed pages, and retry counts. No invented benchmark can replace that evidence.
But don't users need the complete document immediately?
They need honest progress immediately. They rarely need every page rasterized before the first preview appears.
Return the first useful image, show that searchable text is processing, and let the client poll or receive completion through the platform pattern you already operate. The final document can become searchable when the job completes. This preserves quick feedback without pretending a 400-page OCR workload is interactive.
The other common objection is image quality. Rendering at display size sounds like a quality sacrifice, but it is a scope match: the preview uses screen dimensions, while the OCR job may require a different resolution chosen through testing. Keep those outputs separate. Reusing a print-resolution OCR image for a tiny thumbnail makes the interactive path pay for quality it cannot display; reusing a small thumbnail for OCR can damage recognition. One source file, two purposes, two conversion plans.
There is a final operational trade-off. A queue absorbs large jobs, but it introduces queue-age monitoring, retry policy, duplicate-delivery handling, and lifecycle cleanup. Accept that complexity only on the batch path, where it buys isolation and controllable throughput. Keep preview generation small enough that its alert means something.
The decision rule is concise: render what the user can see; schedule what the system must finish. Track conversion duration on both paths. Page count and resolution then become controlled inputs instead of a timeout mystery.
Top comments (0)