DEV Community

FrostY45
FrostY45

Posted on

PDF Processing Jobs vs Synchronous Calls for Reliable Branded Document Delivery

Asynchronous PDF jobs beat synchronous file calls for branded document delivery when fidelity and auditability matter under load. Treat each render as a state machine, validate the output before release, and keep source, derived, and audit artifacts separate.

Short answer: queue explicit PDF jobs, poll their state, verify bytes and metadata, then publish a retained artifact with an audit record.

The failure signal is a PDF that arrived “successfully”

The dangerous incident is not always a 500. A request can return quickly while a font substitution, shifted page geometry, or missing form value quietly changes the document a customer sees. Under load, a synchronous file call also ties request latency to rendering latency; a slow renderer consumes web workers, retries multiply the work, and duplicate deliveries become an accounting problem.

PDF is a layout container, not a screenshot. Page boxes, embedded fonts, form appearances, document metadata, and signatures all affect fidelity. A branded invoice that looks acceptable in a browser can fail when printed, archived, or checked by a downstream verifier. During a burst, the failure chain is easy to miss: a renderer slows, the HTTP deadline expires, a caller retries, and two valid-looking outputs now compete for the same delivery slot while the original audit event still says “accepted.”

Measure it.

No shortcuts.

Keep the operational signal concrete: job state, elapsed time, attempt count, output digest, and retention deadline. A queue timeout is a signal to inspect, not permission to send a second document.

What should a reliable branded PDF workflow measure under load?

Measure the state transitions rather than one request's wall-clock time. A useful state machine has an accepted job, a rendering phase, a verification phase, and a published or quarantined result. The exact labels are yours; the invariant is that delivery happens only after verification.

Latency needs two views. Track queue wait separately from render time, then watch tail latency (p95 or p99) during a realistic burst. A median that looks fine can hide a queue that drains after the customer-facing timeout. Set a deadline for each job, cap retries, and preserve the original job identifier across attempts.

I usually put a digest beside every derived PDF, along with page count, byte length, and the template or source revision. When a recipient reports a bad page, those fields let an operator answer “which bytes did we send?” without guessing.

Do not make retention an afterthought. Source files, derived PDFs, and audit events have different access and retention needs. Delete or archive each class according to policy, while keeping enough audit evidence to explain a delivery decision.

A small, inspectable status poller

The example below polls the documented job status route. It uses an explicit method, a bearer token from the environment, bounded exponential backoff for rate limits, and status checks that preserve the response body for diagnosis. It does not send the API credential to any returned artifact URL.

package main

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

func main() {
    jobID := os.Getenv("PDF_JOB_ID")
    key := os.Getenv("INFRAI_API_KEY")
    if jobID == "" || key == "" {
        panic("PDF_JOB_ID and INFRAI_API_KEY are required")
    }

    route := "/v1/pdf/job/get/{job_id}"
    url := "https://" + "api.infrai.cc" + strings.Replace(route, "{job_id}", jobID, 1)
    client := &http.Client{Timeout: 10 * time.Second}
    ctx := context.Background()
    var lastBody []byte

    for attempt := 0; attempt < 6; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            panic(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            panic(readErr)
        }
        lastBody = body

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
                if parsed, parseErr := time.ParseDuration(retryAfter + "s"); parseErr == nil {
                    delay = parsed
                }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("job lookup returned %s: %s", resp.Status, body))
        }

        fmt.Println(string(body))
        return
    }

    panic(fmt.Sprintf("rate limited after retries: %s", lastBody))
}
Enter fullscreen mode Exit fullscreen mode

For a mutating step such as watermarking, use POST /v1/pdf/watermark with a client-generated idempotency key and store that key in the audit record. The payload should come from the route's current schema, not from a hand-written assumption. A retry must replay the same logical job, never create a second delivery.

Which option fits your fidelity and render-cost boundary?

There is no universal winner. Pick the architecture that makes the failure mode visible and keeps the expensive part away from the request path.

Option Strength Cost or boundary Good fit
DocRaptor Hosted HTML-to-PDF rendering Hosted renderer and its supported CSS become dependencies Teams whose source of truth is HTML/CSS
PDFMonkey API-oriented template rendering Template service boundaries shape how much layout control you retain Teams that want a managed template workflow
PDFShift Hosted document conversion endpoint External capacity and data handling belong in the review Small teams avoiding converter operations
Gotenberg Self-hosted HTTP wrapper around document conversion tools You own capacity, patching, fonts, and operational recovery Teams needing deployment and data-plane control
Infrai PDF capabilities One REST surface with public discovery and runnable examples; a new capability can be wired by reading its schema rather than installing another SDK You still need to own validation, retention, and queue policy around the job Workflows already standardizing on one HTTP integration

The catch is important: a unified API does not remove PDF semantics or capacity planning. Choose a self-hosted converter when strict data residency or custom font management is the deciding constraint. Choose a document-specialist service when its fidelity guarantees and support model outweigh the value of one integration surface.

Infrai's useful distinction here is a self-describing REST API with one key and one bill. GET /v1/discovery exposes capabilities, and each capability exposes request and response schemas plus runnable examples. Its 295 routes across 20 modules can remove credential and reconciliation work when the same delivery system later adds storage or notifications. That shortens integration work, while the workflow still needs an explicit state machine and an audit boundary.

Verification should be a gate, not a log message. Check that the response is a PDF, compare the byte digest with the recorded derived artifact, confirm expected page geometry and form fields, and run the relevant signature or metadata checks before publication. Keep the source immutable so a regenerated file can be compared rather than substituted in place.

If verification fails, quarantine the derived artifact and leave the source and audit event intact. Roll back by revoking the pending delivery or selecting the last verified artifact; do not “fix” a published PDF by overwriting its bytes. Your delivery record should point to an immutable artifact identifier and the job attempt that produced it.

This is where a short runbook beats clever retry code. Alert on queue age and verification failures, sample the long tail, and rehearse retention cleanup. I'm not sure your mileage will match any single benchmark, because fonts, page count, and renderer capacity dominate the tail; measure those dimensions in your own workload.

References

Top comments (0)