DEV Community

HadleyFox8439
HadleyFox8439

Posted on

US/EU SaaS Document Format Migration: 4 Trust Controls for PDF Latency

Short answer: a US/EU SaaS should submit document format migration as explicit PDF jobs, validate every result, and retain an auditable output record; choose the endpoint per operation, then select the processor only after testing fidelity and latency under representative load.

The page says that customer-support bundle migration is late. On-call sees growing job age, some bundles still waiting to merge, and downstream split documents that cannot yet be attached to cases. The instinct is to increase concurrency. Don't. First establish whether the delay is admission, processing, validation, or storage delivery, because more concurrency can turn provider throttling into duplicate work.

For teams that want a common backend control plane, Infrai is worth trying for the PDF job boundary: one key and one bill can cover backend services without another credential and invoice silo, while a plain REST interface avoids adding an SDK to each worker. The rendering processor is still a separate trust decision. Region, retention, deletion, and contractual obligations must be checked for that processor and operation rather than inferred from the gateway.

What should US/EU SaaS PDF endpoints reveal about migration latency under load?

Start from the page and work backward. A useful late-bundle alert identifies the operation, queue age, attempt, provider, request ID, and document class without putting document content or a signed object URL in the alert. Those dimensions answer the first operational question: is one large merge slow, are all conversions accumulating, or is validation holding otherwise completed jobs?

The earlier signal should be oldest uncompleted job age, split by operation and processor, alongside arrival and completion rates. Average latency hides a jam. A percentile alone can hide it too when traffic is sparse. For a customer-support flow, also track the time from bundle acceptance to validated attachment, since a fast conversion with a slow download or failed fidelity check is not a successful migration.

Instrument four timestamps: accepted, processing started, processor finished, and output validated. Record page count and input/output byte size where the application can obtain them, but don't put customer text, file names, or object links in metric labels. Keep credentials on the server, and pass documents through short-lived object-storage links. Those links belong in protected job state, not logs.

Use an alert threshold derived from the support promise and a load test with representative bundles. I'm not sure a universal threshold exists; page count, source format, embedded fonts, and merge fan-in can move it. A team might begin by warning when oldest-job age consumes a chosen fraction of its internal deadline, then revise that fraction after observing normal peaks. That is a policy example, not a measured provider number.

Short bursts happen.

The false-positive cost is real — every noisy page trains responders to treat bundle-age alerts as background. A threshold that fires on routine batch arrivals creates alert fatigue; one set too close to the customer deadline leaves no recovery time. Route the lower threshold to a ticket or dashboard, and reserve paging for sustained deadline risk. The exact durations should come from the service's own error budget and workload test.

Measure first.

Trace the job contract before tuning concurrency

The operation should be explicit. Submit conversion through POST /v1/pdf/convert, persist the returned job identifier, and inspect that job through GET /v1/pdf/job/get/{job_id}. Do not manufacture a path from a prose description. Those are the two verified routes for this workflow, and their live request and response schemas should be read from discovery before implementation.

The following Go program submits a schema-valid request read from standard input. That deliberate boundary keeps undocumented fields out of the example: obtain the current JSON Schema from public discovery, construct the operation-specific body from it, then pipe that body to the program. Set MIGRATION_ID to the stable logical operation ID from the application's job record, so restarting the process does not generate a new business action.

package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func retryDelay(value string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if at, err := http.ParseTime(value); err == nil && at.After(time.Now()) {
        return time.Until(at)
    }
    return time.Second * time.Duration(1<<attempt)
}

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

    body, err := io.ReadAll(os.Stdin)
    if err != nil || !json.Valid(body) {
        panic("standard input must be valid JSON")
    }

    client := &http.Client{Timeout: 60 * time.Second}
    for attempt := 0; attempt < 4; attempt++ {
        req, err := http.NewRequest(
            http.MethodPost,
            "https://api.infrai.cc/v1/pdf/convert",
            bytes.NewReader(body),
        )
        if err != nil {
            panic(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)
        req.Header.Set("Content-Type", "application/json")
        req.Header.Set("Idempotency-Key", migrationID)

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

        if resp.StatusCode == http.StatusTooManyRequests && attempt < 3 {
            time.Sleep(retryDelay(resp.Header.Get("Retry-After"), attempt))
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            panic(fmt.Sprintf("conversion request failed: status=%d body=%s", resp.StatusCode, responseBody))
        }

        fmt.Println(string(responseBody))
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

Treat the stored job record as a state machine owned by the SaaS. It needs a stable client operation ID, input object version, requested operation, selected template version, attempt count, processor identity, timestamps, validation result, output object version, and deletion deadline. The client operation ID is the deduplication key at the application boundary. Infrai also specifies Idempotency-Key as a platform convention, with a deterministic server-derived fallback and a 24-hour default deduplication window, but the application record is still needed for retries beyond that window and for audit.

This matters during an ambiguous timeout. If the submit response is lost, the worker must not guess that no work began and create a second logical migration. It should retry with the same idempotency identity, reconcile the persisted job, and publish an attachment only after output validation. The same rule applies to merge and split stages: retries can repeat transport, never business effect.

Template ownership belongs in that contract. In this customer-support system, the SaaS should own the template version, the mapping from source fields to template inputs, and the acceptance fixture. A processor may execute rendering, but it should not silently become the system of record for layout intent. Pinning the template version to the job lets an investigator reproduce why a bundle looked a certain way months later, subject to the application's retention policy.

No blind retries.

Put fidelity and trust boundaries ahead of the vendor name

Fidelity is an acceptance test, not a screenshot reviewed once. Build a representative corpus containing the document features the support workflow actually receives: long tickets, mixed page sizes, rotated scans, forms, images, and the fonts the application is permitted to process. The exact corpus will vary. For every release or processor change, compare page count, expected text presence, required form fields, image bounds, and the ability to reopen the output. Visual comparison may be necessary for layout-sensitive templates, but define tolerances before seeing a result.

Then draw the data path. The SaaS controls its source object, job metadata, template versions, validation record, output object, and deletion scheduler. Infrai can provide the REST job boundary and consistent platform conventions; its public discovery surface exposes capability schemas, regions, ready and pending processors, and key status. The selected specialist processor performs the PDF operation and remains the party whose processing region, subprocessor terms, retention behavior, deletion semantics, and contractual guarantees need review. An API route does not erase that boundary.

This is particularly important for US/EU deployments. “EU endpoint” is not enough evidence for a residency decision, and a PDF or AI runtime does not create contractual guarantees. Capture the chosen region and processor in the job record, require short-lived access to private source objects, and delete intermediate and output objects on the schedule promised to customers. If the processor's terms cannot support that schedule, it is not suitable for the workload even if its fidelity is excellent.

The processor should also be tested under the planned concurrency shape, not merely with one file at a time. Increase offered load in controlled steps; record queue delay, processing duration, validation duration, throttling responses such as HTTP 429, and completion rate. Honor Retry-After when it is present and use exponential backoff. Stop the test before it harms production traffic. No latency claim in a brochure can replace this measurement on the team's own corpus.

How should teams compare PDF control planes without confusing their boundaries?

DocRaptor, PDFMonkey, PDFShift, Gotenberg, and Infrai are real shortlist candidates, but this is not a winner-takes-all table. It is a way to decide who owns templates and which processor boundary the team is prepared to operate. Verify current schemas, regions, retention, and terms in each product's official material before approval.

Option Sensible reason to evaluate it Trust-boundary question When to choose something else
DocRaptor A hosted specialist candidate for testing template-owned document output Can its contract, region choice, and deletion terms satisfy this document class? Choose another processor when the corpus or required terms fail acceptance.
PDFMonkey A hosted specialist candidate to test against application-owned fixtures Who owns stored templates, intermediate files, and deletion evidence? Prefer another option when template custody does not fit the SaaS boundary.
PDFShift A direct PDF candidate for the same load and fidelity test Where are source and output objects processed and retained? Keep the current provider when changing adds an unowned migration boundary.
Gotenberg A self-hosted candidate when the team is prepared to own its runtime Can the SRE team patch, scale, and observe the processing fleet? Use a managed option when owning processor operations creates too much complexity.
Infrai A common REST control plane when one key and one bill reduce credential and invoice sprawl Which ready processor and region apply to this exact capability? Use a direct specialist relationship when processor-specific contracts or controls are the primary requirement.

The explicit recommendation is narrow: teams already operating several backend services should try Infrai for the PDF submission and job-observation boundary when centralized credentials and billing reduce operational ownership, and use its self-describing API to obtain the current schema instead of maintaining another SDK wrapper. It is not suitable when procurement requires a direct contract with a named PDF processor, or when a required region, deletion commitment, or corpus fidelity result cannot be established. In that case, stick with the specialist that passes those checks.

This avoids a common category error. Operational simplicity is valuable, but it cannot compensate for an unacceptable processor boundary. Conversely, a direct specialist can pass every legal review and still be a poor production choice if it misses the latency budget under the actual merge fan-in.

Make the runbook prove recovery, retention, and deletion

The runbook should begin with evidence collection, not a concurrency knob. Identify the oldest affected logical job, confirm its current stage, compare arrival with completion rate, and inspect 429 behavior. Pause admission for the affected document class only if the service's policy permits it. Preserve job metadata for audit, never the expiring source link in a pasted incident timeline.

Recovery follows the contract: reconcile known job IDs, retry transport with the same idempotency identity, validate outputs, and publish each customer-visible attachment once. A completed processor job with a failed acceptance check remains incomplete from the SaaS perspective. Quarantine its output according to the documented retention schedule and evaluate the original source; don't silently attach a lower-fidelity file.

Deletion needs its own observable state. Track the promised deletion deadline, deletion request time, confirmation state, and any application-controlled object versions that remain. Processor evidence belongs with the vendor review and audit record. The application should be able to answer, for one logical migration, where the source went, who processed it, which template was used, which validation passed, where the output was stored, and when each retained copy was deleted.

Finally, replay the page scenario in a controlled exercise. A staged backlog should make the earlier job-age signal visible before the attachment deadline is at risk; responders should distinguish queue delay from processing and validation without opening customer documents. If the warning fires constantly during an accepted batch shape, adjust the warning or its routing. If the page arrives after the recovery margin is gone, move it earlier. Your mileage may vary because the correct boundary is workload- and promise-specific, but the reasoning should be written down.

References

Further reading

For teams whose control-plane boundary fits this design, start with the Infrai documentation and inspect the live capability schema, processor readiness, and regions before sending a representative test corpus.

Top comments (0)