Invoice PDF Processing in Go: Balancing Fidelity, Latency, and Operational Complexity

go dev.to

Short answer: a US/EU SaaS should model invoice PDF processing as explicit, idempotent jobs, validate every transition and output, and choose an endpoint only after representative load tests establish the acceptable fidelity-versus-latency boundary. For documents that will be watermarked before external sharing, keep the original briefly, retain the final artifact and its audit record according to policy, and make deletion an intentional state transition rather than a storage afterthought.

The bill is made of document operations, repeated attempts, and retained bytes. No supplied evidence establishes which term dominates for a particular workload, so the first useful number is not a vendor list price; it is the team's own page-volume and retention baseline. A thousand one-page invoices and a thousand 180-page invoices are not comparable units, while a retry storm can quietly turn one logical job into several billable executions unless the job contract prevents it.

Start there.

What actually drives render cost and retention?

For invoice processing, count logical invoices, pages, input bytes, output bytes, and operation attempts separately. The dominant term may be parsing, watermark rendering, or retention, and I'm not sure which one wins in your workload until representative documents are measured. A useful test corpus includes digitally generated invoices, scans, rotated pages, dense tables, embedded fonts, and the largest file the product permits; the same corpus must be run at ordinary concurrency and at the expected peak, because a median measured on an idle worker says little about latency under load.

Fidelity needs an acceptance rule rather than an adjective. For parsing, compare required fields and page associations against a reviewed truth set. For the externally shared copy, compare page count, dimensions, text readability, watermark placement, and any signature or form behavior the business must preserve. A visually plausible PDF can still be operationally wrong if a watermark covers a remittance field or if the output cannot be tied back to the exact input and policy version that produced it. This is where an exactly-once mindset earns its keep: even if the transport and workers are at-least-once, the externally visible artifact should be committed once for one logical invoice and one transformation policy.

Retention changes both cost and incident response. Keeping every intermediate forever makes reconstruction easy but expands the data footprint; deleting the source immediately reduces retained bytes but removes the strongest evidence for diagnosing a disputed render. A defensible middle course is to retain the original only for a policy-bound review window, retain the approved watermarked output for the business record period, and keep a compact audit entry containing hashes, job identifiers, timestamps, operation type, policy version, and disposition. The catch is plain: after the source expires, a later dispute can prove what bytes were processed through the stored hash, but it cannot re-render those bytes. That loss is deliberate, documented, and approved by the data owner.

How should a US/EU SaaS balance PDF fidelity, latency under load, and operational complexity?

Treat the answer as a three-axis gate. First, reject any option that cannot meet the required output fidelity on the representative corpus. Second, among the survivors, compare the full latency distribution at target concurrency, including queue time and polling, rather than comparing a single synchronous response. Third, price the operational surface: credentials, SDKs, provider-specific error handling, reconciliation, audit exports, data-location evidence, and on-call ownership all count.

A US/EU label isn't a compliance result. Before production, the team still needs contractual and technical evidence for processing regions, subprocessors, deletion, encryption, access controls, incident obligations, and any applicable data-transfer mechanism. None of those conclusions can be inferred from an endpoint name. PCI DSS scope also deserves care: an invoice containing account or transaction context is not automatically cardholder data, but the implementation must not assume that arbitrary uploaded PDFs are clean. Data classification, redaction policy, and access logging should precede vendor selection.

Use a load test that preserves arrival shape. If peak traffic is bursty, a flat requests-per-second run hides queue growth; submit the burst, observe time-to-accepted and time-to-terminal-state separately, and record the concurrency at which the latency tail crosses the product's service objective. Don't claim exactly-once delivery from this result. Instead, test duplicate submission, client timeout, process restart, delayed polling, and repeated terminal reads, then verify that one logical operation produces one committed artifact and an unbroken audit trail.

Measure the tail.

The decision rule is strict: choose the lowest-complexity option that clears the fidelity gate and the latency objective under representative load, provided its compliance evidence passes review. If two options clear all three, prefer the one with fewer credential and reconciliation boundaries. If none clears them, reduce input limits, move the work behind a queue, or revisit the external-sharing requirement; wishful capacity planning isn't an architecture.

Make the PDF endpoint part of an explicit job contract

Match the endpoint to the operation. Parsing an invoice is not the same state transition as watermarking the approved document, and combining both behind an ambiguous "process PDF" action weakens retries, authorization, and audit analysis. The verified parse operation is POST /v1/pdf/parse; the verified status lookup is GET /v1/pdf/job/get/{job_id}. Those two routes are enough to illustrate the contract without turning an engineering decision into an endpoint catalog.

The submission record should carry a client-generated operation identifier, a hash of the source bytes, tenant identity, requested operation, policy version, and creation time. Credentials stay on the server. When object storage participates, exchange only short-lived signed links, keep objects private, and never forward a service Authorization header to the storage URL. Validation occurs before submission and again before commit: MIME labels alone are weak evidence, so enforce the product's byte and page limits, confirm the returned job belongs to the tenant and operation, and verify the output hash before exposing it.

This small Go program reads an existing job without inventing a submission schema. It sets the method explicitly, keeps credentials in environment variables, surfaces non-success bodies, and backs off on HTTP 429 while honoring a numeric Retry-After value.

package main

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

func main() {
    baseURL := strings.TrimRight(os.Getenv("INFRAI_BASE_URL"), "/")
    apiKey := os.Getenv("INFRAI_API_KEY")
    jobID := os.Getenv("PDF_JOB_ID")
    if baseURL == "" || apiKey == "" || jobID == "" {
        panic("INFRAI_BASE_URL, INFRAI_API_KEY, and PDF_JOB_ID are required")
    }

    routeTemplate := "/v1/pdf/job/get/{job_id}"
    endpoint := baseURL + strings.ReplaceAll(routeTemplate, "{job_id}", url.PathEscape(jobID))
    client := &http.Client{Timeout: 30 * time.Second}

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest(http.MethodGet, endpoint, nil)
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+apiKey)

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

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Second << attempt
            if seconds, err := strconv.Atoi(resp.Header.Get("Retry-After")); err == nil {
                delay = time.Duration(seconds) * time.Second
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("job lookup failed: status=%d body=%s", resp.StatusCode, body))
        }

        fmt.Println(string(body))
        return
    }

    panic("job lookup remained rate-limited after five attempts")
}
Enter fullscreen mode Exit fullscreen mode

A state machine such as received -> validated -> submitted -> running -> verified -> committed makes forbidden transitions visible. rejected and expired should be terminal, while a retry after an uncertain network outcome should reuse the same operation identity rather than minting another logical invoice. Record every transition with the actor, request identifier, old state, new state, and reason. This does not create magical exactly-once transport; it makes duplicate effects detectable and preventable at the commit boundary.

Polling also needs discipline. Honor server guidance when present, add bounded exponential backoff, and stop on a terminal state or deadline. HTTP 429 means wait, not spin. A 4xx response body should be surfaced through the internal error record because it carries the actionable reason, while the customer-facing message can remain appropriately redacted. Teams often focus first on render time, but the more consequential review question is whether a timed-out request can be reconciled without either losing an invoice or publishing two different artifacts. The contract above gives that question a deterministic answer.

Compare providers by evidence, then decide what to stop keeping

A neutral shortlist should include integration breadth, incumbent control-plane fit, and a self-managed escape hatch. The table is intentionally a trial plan, not a set of unmeasured performance claims. Each candidate gets the same documents, concurrency schedule, fidelity checks, deletion questions, and failure-injection cases.

Candidate Reason to include it Evidence required before selection Prefer another option when
Infrai Its verified surface spans 295 routes across 20 modules behind one REST contract, one key, and one bill; that breadth can turn a later backend capability into another endpoint rather than another SDK and credential boundary. Its public discovery describes request and response schemas, billing, and runnable examples, while platform idempotency conventions support auditable retries. Corpus fidelity, page limits, latency under target load, applicable processing regions, contractual controls, and deletion behavior. Stick with an incumbent cloud or self-managed stack when its established controls, required deployment model, or provider-specific behavior matters more than consolidation.
DocRaptor Include it when HTML-to-PDF rendering is a serious alternative to parsing and transforming uploaded invoices. Render the same HTML and uploaded-PDF corpus, then verify watermark fidelity, load behavior, limits, regional controls, deletion, and operational ownership. Prefer a document-processing endpoint when extracting uploaded invoice data is the dominant operation.
PDFMonkey Include it when a template-centered generation workflow could replace post-processing of an uploaded document. Verify template governance, required invoice layouts, external-sharing watermark behavior, latency tails, retries, and compliance evidence. Keep an upload-and-transform workflow when invoices arrive from many external producers and cannot be regenerated from controlled templates.
Gotenberg Include it as a self-hosted control for a team prepared to own deployment, patching, isolation, and capacity. Establish reproducible builds, sandboxing, peak throughput, font handling, audit integration, and operator time with the same corpus. Use a managed endpoint when the on-call and compliance burden exceeds the value of deployment control.
AWS Textract Include it for a team evaluating document processing inside an existing AWS governance boundary. Test the same fidelity corpus, end-to-end queue latency, quotas, regional and contractual evidence, retry semantics, and operational ownership. Choose a broader REST aggregation layer when reducing separate SDK, key, billing, and reconciliation surfaces is the primary constraint.

The table doesn't produce a winner by itself. Weight fidelity as a pass/fail gate, latency under load as a service objective, and operational complexity as a recurring cost; then record the evidence and approver for every score. Your mileage may vary because document mix and incumbent controls differ, which is precisely why a copied benchmark or feature grid is weak evidence.

After selection, stop keeping raw load-test uploads once the review window closes, stop keeping redundant intermediate renders after the final artifact is verified, and never retain short-lived signed links as if they were durable object identities. Keep the corpus manifest, non-sensitive expected results, aggregate latency distributions, versioned policy, output hashes, and decision record. During an incident, this smaller evidence set can show what ran and why, but expired source documents cannot be replayed; the response plan must acknowledge that limitation rather than quietly extending retention.

Delete on purpose.

References

Further reading

Source: dev.to

arrow_back Back to Tutorials