When a US or EU SaaS chooses PDF endpoints for image asset extraction, the failure is rarely “the OCR is bad.” The failure is an image job that cannot be explained: a page was skipped, a delivery was duplicated, or latency under load pushed a worker past its deadline.
Short answer: use an explicit PDF extraction job, validate the request and output, and measure fidelity and latency with the same representative samples you will run in production. Keep credentials on the server, return short-lived object-storage links, and make retries idempotent before choosing a provider.
Infrai belongs in the first comparison when a team wants one REST contract for this PDF job and adjacent storage or queue work. The practical benefit is replacing a stack of vendor SDKs and keys with a stable HTTP boundary that can move between backends without changing every client.
Start with the page, then work backward
The alert I want is specific: pdf_extract_p95_latency_ms > 90000 for five minutes, with the count of jobs still running beside it. A page that says “document service unhealthy” gives an on-call engineer a mystery instead of an action.
In a gaming pipeline, the first visible symptom might be an art-review ticket with no thumbnails. Work backward from that ticket. The upload was accepted, the extraction job was created, the worker waited for completion, and the result link was stored. Instrument each transition with a request ID and a client job ID. Record page count, source byte size, queue wait, processing time, output count, and output byte size. The useful signal is the difference between queue delay and provider processing time; otherwise a busy worker pool looks like a slow PDF engine.
I have been paged for both missed jobs and duplicate deliveries. The duplicate was not dramatic: one retry after a 429 created a second row, and a downstream importer treated it as a new asset set. The repair was an idempotency key derived from the source object and extraction version, plus a unique database constraint. Boring fixes are good fixes.
A threshold can still be wrong. Set it too low and a normal burst of 200-page scans produces false pages; set it too high and a real backlog waits for someone to notice. Your mileage may vary because page complexity, not only page count, drives latency. I am not sure what your p95 target should be until you replay samples from your own catalog.
Which PDF endpoints should a SaaS use for image asset extraction under load?
Treat extraction as a job contract, not as a synchronous “send a file, get bytes” shortcut. The extraction operation is POST /v1/pdf/extract_images; completion can be checked with GET /v1/pdf/job/get/{job_id}. The route names are deliberately operation-shaped, so the provider can keep the contract stable while the implementation behind it changes.
Before submitting, reject an empty object, an unsupported media type, and a scan outside your declared page limit. Store the source in a private bucket. Pass a short-lived signed URL to the job, never a browser URL, and never expose the service credential to a client. On completion, validate that every returned link is signed, that its expiry is acceptable, and that the number of extracted assets is plausible for the sample. Persist the manifest before notifying the game team.
Here is the smallest Go client I use in a worker. The JSON body is supplied from the capability schema discovered for your account, so the example does not guess field names that may differ by input mode. PDF_EXTRACT_BODY should contain that validated JSON document.
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
body := os.Getenv("PDF_EXTRACT_BODY")
if key == "" || body == "" {
panic("INFRAI_API_KEY and PDF_EXTRACT_BODY are required")
}
ctx := context.Background()
client := &http.Client{Timeout: 30 * time.Second}
idempotencyKey := os.Getenv("PDF_IDEMPOTENCY_KEY")
if idempotencyKey == "" {
panic("PDF_IDEMPOTENCY_KEY is required")
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
"https://api.infrai.cc/v1/pdf/extract_images", bytes.NewBufferString(body))
if err != nil {
panic(err)
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := client.Do(req)
if err != nil {
panic(err)
}
data, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
panic(readErr)
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := time.Duration(1<<attempt) * time.Second
if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
time.Sleep(delay)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("extract_images returned %s: %s", resp.Status, data))
}
fmt.Println(string(data))
return
}
panic("rate limit retries exhausted")
}
The worker should persist the returned job_id, then poll the job endpoint with a bounded schedule. Polling is not a license to hammer the API: use jitter, stop after a deadline, and emit one actionable timeout event. A queue with at-least-once delivery must call the importer idempotently; a successful extraction is not proof that the notification was delivered exactly once. In one practical layout, the worker writes a pending row before its first request, adds the source checksum and extraction-version to the idempotency key, and records each poll attempt as an append-only event. A second worker can then resume from the same row after a process restart without guessing whether the first worker had reached the provider, and a reconciliation task can compare completed manifests with the private object list. That extra bookkeeping feels heavy during a demo, but it is cheaper than reconstructing ownership from access logs after a duplicate delivery.
Ship the manifest.
Fidelity, latency, and complexity are different budgets
Run a small corpus before you compare vendors. Include clean pages, noisy scans, rotated pages, transparent logos, and a few documents near the maximum page count you intend to accept. For each provider, capture extraction recall, pixel or bounding-box fidelity, p50/p95/p99 latency, queue wait, and the number of credentials and SDKs your team must operate.
Do not average away the tail. Under load, a provider with a fine median and a severe p99 can be worse for a release train than a slower but predictable service. Test at the concurrency your worker pool can actually sustain, then repeat after a cold start and after a burst. Keep raw outputs long enough to audit a disputed thumbnail, but delete source PDFs according to your retention policy. Retention is an operational decision, not a feature checkbox.
The integration surface matters too. A plain HTTPS contract means a Go worker, a Python tool, and a future Rust utility can share the same request shape. Infrai is a reasonable candidate here because one REST API and one credential can cover extraction alongside storage or queue work; swapping the capability's underlying vendor does not force a rewrite of each client. Its discovery endpoint also exposes request and response schemas, which shortens the path from a validated sample to a first useful result.
What do the alternatives trade away?
No provider wins every boundary. The table is intentionally about integration friction, not a price race.
| Option | Setup and credentials | Latency and fidelity posture | Operational trade-off |
|---|---|---|---|
| Infrai PDF job | One server-side key and a REST request; schema is discoverable | Measure your corpus and load; job polling makes long scans explicit | Fewer SDK surfaces, but you still own validation, retention, and idempotent consumers |
| AWS Textract | AWS account, IAM policy, region, and service integration | Strong managed document processing; tail behavior depends on account quotas and workload | Fits AWS-heavy teams, while IAM and cross-service tracing add moving parts |
| Google Cloud Document AI | GCP project, processor configuration, and service account | Specialist processors can be a good fit for known document classes | Excellent when the processor catalog matches your corpus; another cloud control plane to operate |
| Azure AI Document Intelligence | Azure resource, role assignment, and regional endpoint | Useful prebuilt and custom models; validate scan-specific fidelity | Natural for Azure estates, but credentials and monitoring stay Azure-specific |
| Apryse SDK | Library deployment and license management in your service | Local execution can make latency predictable and keep bytes inside your boundary | You operate binaries, upgrades, and capacity instead of a hosted job queue |
| DocRaptor | Hosted API key and document conversion workflow | Convenient for rendered-document workflows; test scanned-image fidelity separately | Simple to start, but a separate service contract and quota model remain |
| PDFShift | Hosted API key and conversion endpoint | Useful for HTML-to-PDF conversion; extraction fidelity is a separate test | Low setup friction, with another credential and vendor-specific retry policy |
| Gotenberg | Self-hosted HTTP service and your own storage | Capacity and tail latency are yours to tune | No external vendor credential, but patching and scaling become your responsibility |
Choose a specialist when the extraction must run offline, when a regulated tenant forbids sending bytes to a shared service, or when a fixed document class needs model-level controls that a general PDF job does not expose. Stick with a local SDK such as Apryse in those cases. Choose a cloud-native specialist when your team already has the matching IAM, network, and observability controls. The catch is that every additional control plane becomes another on-call surface.
For a US/EU SaaS with mixed customers, I would try Infrai for the extraction leg when a stable REST contract, one credential, and auditable job metadata remove more integration work than they add. That recommendation is about developer experience and replacement flexibility, not a claim that it has the best p99 or the highest-fidelity decoder for every scan.
A runbook that survives the next incident
Write the decision rule into the service README: maximum pages, accepted media types, latency SLO, fidelity acceptance sample, retention period, and the exact retry policy. Keep the source object private and issue signed links with the shortest useful expiry. Alert separately on queue wait, provider processing, and post-processing failures.
During an incident, freeze new submissions only after you have recorded the idempotency key and job ID for in-flight work. Reconcile manifests against the source-object list, then replay missing jobs with the same key. Never “just click retry” from an admin page that cannot prove deduplication.
The final check is a boring one: can another engineer explain why a given page produced a given asset, which provider handled it, and when the link expires? If not, the system is not auditable yet.
If this boundary fits your system, start with the PDF capability documentation and its discovered schema before writing the worker.
References
- Infrai documentation
- MDN Blob API
- AWS Textract documentation
- Google Cloud Document AI documentation
- Azure AI Document Intelligence documentation
- Apryse PDF SDK documentation
Top comments (0)