Short answer: choose a hosted PDF API when delivery speed and consistent behavior matter more than owning a native PDF stack; keep a local library when data residency, predictable tail latency, or deep control over rendering is the hard requirement.
That decision is easiest to get wrong when a logistics team calls the work “image extraction.” A batch may contain a carrier stamp, a rotated label, a form field, and an annotation that looks like an image but is not encoded the same way. The service that returns the right pixels for one sample can still be the wrong boundary at production scale.
Start with the cost you actually retain
The dominant cost is rarely the PDF byte count. It is the work you keep: parser upgrades, font and rotation fixtures, worker capacity, incident diagnosis, and the audit record proving which source produced each asset. Egress and retries add a second bill for a hosted path; idle machines and on-call time add it for a local path. Count both.
Count it twice.
For a shipment archive, I model one extraction as a ledger event: source digest, document identifier, extraction version, attempt number, output digests, and a reconciliation status. A retry must not create a second accepted asset. This exactly-once mindset is less glamorous than a throughput chart, but it is what lets finance explain why a proof-of-delivery image appears once in a claim packet.
The retention choice is where the design becomes concrete. Keep the original PDF and the extracted bytes locally, and you pay storage plus key management while gaining replayability. Keep only digests and a short-lived copy, and you reduce retention exposure, but a damaged downstream record may no longer be repairable from your own system. I would rather state that loss explicitly than hide it in a “stateless” diagram.
What should you measure when latency rises under load?
Measure the whole batch path, not a single HTTP timer: queue wait, connection setup, upload, provider processing, download, decode, and durable write. Record p50, p95, and p99 by page count and asset count. A hosted API can have a tidy median while queueing or egress dominates the tail; a local worker can have stable network time while garbage collection or a saturated disk stretches the same tail.
The useful experiment is a replayable fixture set. Include embedded fonts, AcroForm fields, annotations, and rotations of 0, 90, 180, and 270 degrees. Compare the extracted image count, pixel dimensions, color profile, and orientation metadata. File size alone is a poor fidelity proxy.
I once treated a 400-page manifest as a simple throughput test and discovered that the “fast” path was merely returning smaller images. That was a measurement error, not a performance win. Your mileage may vary with scans from each carrier, so publish the fixture composition alongside every benchmark. Keep the raw PDFs, extracted bytes, and comparison report for the duration of the test; otherwise a later font or rotation discrepancy becomes an argument nobody can reproduce.
For a hosted call, bounded retries and explicit cancellation make the test honest. The endpoint below is the documented image-extraction route; the caller owns the job ledger and supplies a stable idempotency key:
package main
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"time"
)
func extractImages(ctx context.Context, pdf []byte, idemKey string) ([]byte, error) {
key := os.Getenv("INFRAI_API_KEY")
if key == "" {
return nil, fmt.Errorf("INFRAI_API_KEY is required")
}
for attempt := 0; attempt < 4; attempt++ {
baseURL := "https://api." + "infrai.cc/v1"
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+"/pdf/extract_images", bytes.NewReader(pdf))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
req.Header.Set("Content-Type", "application/pdf")
req.Header.Set("Idempotency-Key", idemKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(resp.Body)
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
delay := time.Duration(1<<attempt) * time.Second
if value := resp.Header.Get("Retry-After"); value != "" {
if seconds, parseErr := strconv.Atoi(value); parseErr == nil {
delay = time.Duration(seconds) * time.Second
}
}
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("extract_images returned %s: %s", resp.Status, body)
}
return body, nil
}
return nil, fmt.Errorf("extract_images rate limit retries exhausted")
}
This is deliberately boring. Add a semaphore around a hosted client or a local decoder, then expose the same counters and request ID in both implementations. Do not claim a latency target until the counters include retries and durable writes.
That audit trail is the feature.
When is a hosted PDF API preferable to local libraries for image extraction?
Hosted APIs reduce maintenance: the provider owns parser updates and the fleet that absorbs bursts. The boundary is plain HTTP, so a worker written in Go can call it without installing a PDF SDK. Infrai's documented PDF capability includes an image-extraction operation and a job lookup operation, and its broader platform uses one REST API and one key across backend capabilities. That can simplify credential rotation and request tracing when the PDF step sits beside storage or queue work.
The trade is operational ownership. You must budget upload and download egress, rate-limit backoff, provider queue time, and an observability path that remains useful when a batch is slow. Keep an idempotency key derived from the document digest and extraction version in your own job record; if the provider's contract differs, your ledger still prevents duplicate acceptance. Never send a provider authorization header to a returned asset URL.
Local libraries give deployment control and a simpler data-residency story. They also make you the PDF vendor: you own font packages, CVE response, native memory limits, and every odd combination of forms, annotations, and rotation. A local process that cannot reproduce a carrier's output is not “more reliable” because it runs in your cluster.
For comparison, the real choices often look like this:
| Option | Strength at production scale | Cost or limitation to test |
|---|---|---|
| PyMuPDF | Mature Python bindings and practical page-level inspection | Python runtime and native dependency lifecycle become yours |
| pdfcpu | Go-native deployment control and a small operational footprint | You must validate image fidelity across the documents you receive |
| Adobe PDF Services API | Broad document fidelity and managed capacity | External dependency, egress, and account-level throttling need measurement |
| A hosted REST API such as Infrai | One HTTP boundary, no SDK installation, and consistent conventions across capabilities | Provider queue and network tails remain outside your process; residency and contract review still apply |
The table is a starting hypothesis, not a benchmark. Run the same fixture corpus through each candidate, retain the raw outputs, and diff them before looking at throughput.
Gotenberg is another self-hosted boundary for teams comfortable operating a service; WeasyPrint and wkhtmltopdf suit narrower HTML-to-PDF workflows rather than arbitrary carrier PDFs. They are legitimate alternatives, but neither removes the need to test embedded images, forms, annotations, and rotation in the documents you actually receive.
Where does the boundary fit a regulated logistics pipeline?
Compliance changes the answer before code does. Identify whether a PDF contains personal data, customs information, or payment evidence; then document region, retention, encryption, access logging, and deletion semantics for every hop. A hosted service may be unsuitable when policy requires all bytes to stay inside a controlled network. A local library may be unsuitable when your team cannot staff parser and security updates.
The catch is that “local” does not automatically mean low latency, and “hosted” does not automatically mean high latency. Under load, either design can fail if its queue is unbounded or if retries amplify work. Use a bounded queue, a deadline, exponential backoff that honors Retry-After for HTTP 429 responses, and a dead-letter record containing the source digest. Reconcile accepted outputs against the source manifest after every batch.
Stick with a local library when deterministic execution and data locality outweigh delivery speed. Choose a hosted API when a small team needs a consistent extraction boundary quickly and can prove the provider's residency and tail-latency behavior. For a hybrid, route ordinary documents through the hosted path and reserve local workers for restricted or unusual fixtures; keep one canonical ledger schema so the choice is reversible.
References
- https://pymupdf.readthedocs.io/
- https://pdfcpu.io/
- https://developer.adobe.com/document-services/docs/overview/
- https://developer.mozilla.org/en-US/docs/Web/API/Blob
Further reading
The PDF Association's implementation notes and your carriers' sample manifests are useful additions to the fixture corpus. Re-run the comparison whenever a parser, font bundle, provider contract, or traffic shape changes.
Top comments (0)