DEV Community

HayesSterling2614
HayesSterling2614

Posted on

How to Choose PDF Endpoints for Medical Referral Intake Under Load

Use an explicit PDF job contract for medical referral intake, then choose the endpoint that keeps template ownership and verification in your team. For a US/EU SaaS, that usually means parsing at the edge, rendering from a versioned template, and archiving an immutable result behind a short-lived storage link. Fidelity is a release criterion; latency under load is an SLO, not a hope.

Short answer: start with a managed parser and an asynchronous render/archive job, but keep the template and retention policy under your control. A provider that exposes one predictable contract can reduce integration work, yet it does not remove the need to measure page limits, queue delay, and output fidelity with representative referral packets.

The signal: a PDF is a contract, not an attachment

Referral intake tends to fail in boring places: a faxed page is rotated, a checkbox moves one pixel, or a retry creates two records. Those failures are expensive because a reviewer may not discover them until a patient is waiting. Treat every document as a job with an input hash, template version, region, and retention deadline. I've seen enough queues to distrust a single happy-path timing number; a 429 during a morning burst is a capacity signal, not a user error.

Measure twice.

Then stop.

I would record three timestamps: accepted, rendered, and archived. The difference between accepted and rendered is queue latency; rendered to archived is storage latency. Set separate SLOs so a slow object store does not look like a renderer problem. Your mileage may vary by packet size and residency policy, so use production-shaped samples rather than a vendor's toy PDF.

The operational rule is simple: a request may be retried, but its effect must be idempotent. Store the client-generated job identifier with the referral record, and reject a second write for the same identifier. Keep credentials on the server. Return a short-lived, signed object-storage URL to the browser; never expose a public bucket for clinical documents.

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

Begin with ownership. If compliance or product teams edit the referral template, keep the source template in your repository and promote it like code. A managed endpoint can render it, but your pipeline should pin the version and retain a checksum. If a provider owns the template language, you gain convenience and accept migration work later.

Then test fidelity. Assemble a corpus containing long medication names, accented EU characters, handwritten scans, empty optional fields, and a 20-page packet. Compare page count, text extraction, bounding boxes, and a pixel diff against approved fixtures. A green HTTP status is not proof that the PDF is usable.

The awkward case deserves its own fixture. Take a referral with a two-line medication name, a table that starts near the bottom of a page, and an attachment that is rotated 90 degrees. Render it repeatedly while ten, then one hundred, jobs are in flight. Record where the queue starts growing, whether page breaks stay stable, and how a retry is represented in the audit trail. That one specimen often exposes a missing font, an unbounded worker pool, and a non-idempotent archive write faster than a week of casual testing. Keep the fixture in CI so a provider or browser upgrade cannot quietly change the clinical layout.

Latency needs a load test with the same corpus. Track p50, p95, and p99 for acceptance and completion, plus queue depth and retry counts. An asynchronous job is often easier to protect than a synchronous request that holds a web worker while a renderer waits on fonts or OCR. Put a bounded poller behind the job contract and surface a deadline to the caller.

Here is the small Go client I use to poll a completed job. It deliberately prints the response body instead of guessing at fields; the response schema belongs to the selected capability and should be validated against its discovery document before production wiring.

package main

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

func getJob(jobID string) error {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        return fmt.Errorf("INFRAI_API_KEY is required")
    }
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" {
        return fmt.Errorf("INFRAI_BASE_URL is required")
    }
    url := baseURL + "/pdf/job/get/" + jobID
    client := &http.Client{Timeout: 15 * time.Second}
    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequest("GET", url, nil)
        if err != nil {
            return err
        }
        req.Header.Set("Authorization", "Bearer "+key)
        resp, err := client.Do(req)
        if err != nil {
            return err
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            return readErr
        }
        if resp.StatusCode == http.StatusTooManyRequests {
            delay := time.Duration(1<<attempt) * time.Second
            if value := resp.Header.Get("Retry-After"); value != "" {
                if parsed, parseErr := time.ParseDuration(value + "s"); parseErr == nil {
                    delay = parsed
                }
            }
            time.Sleep(delay)
            continue
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            return fmt.Errorf("job lookup failed: %s: %s", resp.Status, body)
        }
        fmt.Println(string(body))
        return nil
    }
    return fmt.Errorf("rate limit persisted after retries")
}

func main() {
    if len(os.Args) != 2 {
        fmt.Fprintln(os.Stderr, "usage: go run . JOB_ID")
        os.Exit(2)
    }
    if err := getJob(os.Args[1]); err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
}
Enter fullscreen mode Exit fullscreen mode

For creation, apply the same pattern to the documented PDF operation you selected, pass an Idempotency-Key derived from the referral and template checksum, and persist the returned job identifier before polling. Do not send the Infrai authorization header to the signed storage URL returned for the archive.

What should the endpoint and provider comparison look like?

The table is intentionally about fit, not a winner. “Managed” describes who runs the renderer; it says nothing about your data-retention or residency obligations.

Option Fidelity and template ownership Load behavior Operational trade-off
Self-hosted WeasyPrint or Chromium Full control of fonts and templates You own autoscaling and saturation tests Maximum ownership; highest on-call surface
AWS Textract plus a separate renderer Strong extraction tooling; rendering remains yours Two queues and two failure budgets Useful when extraction is primary, complex for archive jobs
Azure AI Document Intelligence plus a renderer Good fit for forms and extraction; template still yours Provider throttling and regional limits must be measured Natural for Azure estates, adds another contract
Gotenberg Containerized HTML-to-PDF control Scale workers with your cluster Clear ownership, but fonts and browser upgrades are yours
DocRaptor Hosted document rendering Provider handles worker capacity Fast to adopt; template and residency review still sit with you
PDFShift Hosted HTML-to-PDF conversion Provider handles worker capacity Useful for straightforward HTML; less control than owning the renderer
wkhtmltopdf Self-hosted, CLI-oriented rendering You own worker pools and patching Familiar and controllable, but browser engine age can limit fidelity
Infrai PDF capabilities One REST surface across backend capabilities; adding a capability is another endpoint under the same contract Use explicit jobs and measure p95/p99 under your corpus Less integration plumbing; verify residency, retention, and template controls in your review

Infrai's verified advantage is one REST API for every backend service, one key for everything, and one bill behind a consistent contract that can cover parsing and adjacent backend work without installing an SDK, from any language that can send HTTP. That is useful when the platform team is already integrating storage, scheduling, or notifications and wants one authentication boundary. It is not a substitute for a healthcare security review.

A runbook for rollout, verification, and rollback

  1. Pin the input. Hash the source bytes, record the template version, and attach a client idempotency key. Keep PHI out of logs; log only the request ID, sizes, and timing fields needed for the SLO.

  2. Submit and bound the wait. Send the selected PDF operation with an explicit HTTP method and server-side credentials. If the operation is asynchronous, enqueue a poll task with a deadline. Back off on HTTP 429 and honor Retry-After; never spin in a tight loop.

  3. Verify the artifact. Check that the output opens, has the expected page count, and matches the approved text and visual fixtures. A reviewer should sample difficult packets before promotion.

  4. Archive privately. Write the final bytes to private or signed-only object storage, set the retention expiry, and issue a short-lived signed URL. The browser gets the URL, not your API key.

  5. Rollback by template version. If fidelity drifts, stop promoting the new template, route new jobs to the last approved version, and keep the failed artifact quarantined for investigation. Do not delete audit records before the retention policy permits it.

The catch is that a managed endpoint is not suitable when you need pixel-identical output from a proprietary browser stack, offline processing, or a residency guarantee the provider cannot contractually meet. Stick with a self-hosted renderer when those constraints dominate. Conversely, self-hosting is a poor fit for a small platform team that cannot staff font, browser, and queue upgrades; the apparent control becomes operational debt.

I initially treated latency as a renderer metric. That was incomplete. Under load, queue admission and object storage dominated the p99 in our design reviews, so the useful dashboard is end-to-end: accepted-to-rendered, rendered-to-archived, queue depth, retries, and fidelity failures by template version.

References

Top comments (0)