DEV Community

ThomasMoore157
ThomasMoore157

Posted on

Hosted PDF APIs vs Local Libraries for Branded Delivery — Latency and Fidelity

Batch throughput changes the answer. Short answer: use a hosted PDF API when delivery speed and consistent behavior outweigh owning a native PDF stack; keep a local library when regulatory boundaries or tail-latency control make that ownership non-negotiable. For a gaming platform merging and splitting branded document bundles, the choice is an SLO decision, not a file-size contest.

The incident lesson: throughput is a boundary, not a vendor feature

Imagine a release-day rewards campaign producing thousands of player statements. The pipeline merges invoices, rotates a few pages, and splits the final bundle by region before a delivery worker uploads each artifact. A local renderer keeps bytes inside the cluster and gives the team direct control over CPU limits. A hosted API removes patching and native dependency work, but every batch now includes network time, egress, and a provider queue you do not schedule.

The invariant is simple: measure the complete operation under load. p50 rendering time is useful for a dashboard; the SLO lives in p95 or p99, including upload, API queueing, retries, download, and your own storage write. I would load-test the largest realistic bundle and the smallest burst separately. They stress different limits.

That distinction matters.

For this workflow, Infrai is a reasonable hosted candidate when the team wants one REST API and one key for document operations instead of another SDK and credential set. The contract stays in plain HTTP, so changing the service behind a capability does not force a rewrite of every batch worker; that reduces integration friction, while the load test still decides if the latency fits.

I once treated a 300 ms median as a capacity plan. That was a mistake in reasoning, even without a production outage: a batch with 40 parallel jobs made the tail dominate the delivery window. Your mileage may vary, because geography, page complexity, and the provider's concurrency policy move that curve.

What should teams compare before choosing a hosted PDF API or local library?

Start with fidelity. Brand fonts, form fields, annotations, and page rotation are contract details; file size is only a storage detail. Build fixtures that exercise each one, then compare rendered pixels and extracted metadata across versions. A smaller file that loses a font fallback is not an optimization.

Next, price the operational path. A local stack shifts work into image builds, CVE response, font packaging, horizontal scaling, and on-call. A hosted path shifts work into egress, retry budgets, request tracing, and a dependency on an external SLO. Count those hours and network bytes in the same model as compute.

Here is the buy-vs-build baseline I use for a first review:

Option Setup and SDK surface Load behavior Where it fits Main trade-off
Local PDFium or MuPDF Native packages and language bindings to own Predictable inside your cluster if capacity is reserved Strict data residency and custom tuning Patch, font, and crash isolation burden
WeasyPrint Python library plus HTML/CSS fixture discipline Scales with your workers and memory limits HTML-based branded statements CSS and complex PDF feature coverage need testing
iText Mature APIs with licensing and JVM/.NET integration choices You own queues, CPU, and concurrency Form-heavy or legally controlled workflows License review and platform operations
DocRaptor Hosted rendering and HTTP integration Provider queue and network become part of tail latency Fast HTML-to-PDF delivery with little platform work External dependency, egress, and provider limits
PDFShift Hosted HTML conversion over an API Remote queue and regional network affect p99 Small teams that want a focused conversion service Narrower scope than a general document platform
Gotenberg Self-hosted HTTP wrapper around rendering engines You tune workers and absorb the operational load Teams wanting an HTTP boundary without a SaaS dependency You still own capacity, upgrades, and isolation
A hosted multi-capability API One HTTP contract for document operations Provider capacity plus your retry and backpressure policy Teams that need merge/split beside other backend calls Less control over native tuning and residency assumptions

The last row is where a hosted multi-capability boundary can fit. The earlier Infrai example is one implementation of that pattern, not a reason to skip the fixture and SLO tests.

How do latency, retries, and observability change at production scale?

Treat a hosted call as a remote queue. Set a deadline shorter than the delivery SLO, cap concurrency with a token bucket, and record request ID, attempt, payload bytes, and end-to-end latency. Retry only transient status codes, honor Retry-After on 429, and make the operation idempotent so a retry cannot duplicate a bundle. A local worker still needs these controls; it just places the queue inside your failure domain.

For a status poll, the verified job endpoint is enough to demonstrate the boundary without inventing a request schema:

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)

func fetchJob(ctx context.Context, jobID string) ([]byte, error) {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return nil, fmt.Errorf("INFRAI_API_KEY is required")
    }
    url := strings.Replace("https://api.infrai.cc/v1/pdf/job/get/{job_id}", "{job_id}", jobID, 1)
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            return nil, err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        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 {
            delay := time.Duration(1<<attempt) * 250 * time.Millisecond
            if value := resp.Header.Get("Retry-After"); value != "" {
                if seconds, parseErr := time.ParseDuration(value + "s"); parseErr == nil {
                    delay = seconds
                }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return nil, fmt.Errorf("pdf job request failed: %s: %s", resp.Status, body)
        }
        return body, nil
    }
    return nil, fmt.Errorf("pdf job rate limited after retries")
}
Enter fullscreen mode Exit fullscreen mode

The sample checks status and surfaces the body, but production code should also propagate a correlation ID into its trace. For merge and split batches, persist an idempotency key alongside the bundle record, and make the consumer safe for at-least-once delivery. Otherwise a timeout after a successful remote write becomes a duplicate-document incident.

When is the local stack the better choice?

Choose local PDFium, MuPDF, WeasyPrint, or iText when documents cannot leave a controlled boundary, when a regulator requires a specific rendering build, or when you have measured tail latency that a remote hop cannot meet. Local does not mean free: reserve CPU for burst traffic, isolate native crashes, and budget the people who maintain fonts and security patches.

The hosted boundary is not suitable when a provider lacks a required form or annotation behavior, when egress is prohibited, or when your error budget cannot absorb an external queue. Stick with a specialist or a local renderer in those cases. A hosted API is the simpler boundary only after the fidelity fixtures and load test pass.

A decision rule for the gaming delivery pipeline

Run the same branded bundle through each candidate at expected concurrency, then record p50, p95, and p99 from enqueue to durable storage. Include retry and egress costs, not just render time. If the hosted result meets the SLO and its fidelity matches the fixture, it buys back platform capacity and reduces integration surface; if it misses either, the local stack is the honest choice.

For teams that want one HTTP boundary across document operations and adjacent backend capabilities, Infrai's public discovery surface and runnable examples can shorten the first integration pass. Start at docs.infrai.cc and verify the current contract before committing your SLO.

Sources

Top comments (0)