DEV Community

nilsberg2187
nilsberg2187

Posted on

Reliable PDF Shipping Labels Under Load: Fidelity, Latency, and Operations

Shipping labels are a small document problem with production-sized consequences. My default is an explicit PDF job with strict input checks, an idempotency key, and an auditable output link. That choice usually preserves fidelity better than asking a synchronous renderer to finish inside a checkout request, while keeping latency under load measurable instead of mysterious. The trade is operational work: a queue, a status record, and retention rules.

Short answer: choose a provider that exposes a real PDF job contract, then measure fidelity and tail latency with your actual label samples before committing. Keep credentials on the server, return short-lived object-storage links, and make duplicate delivery harmless.

The incident lesson: a label is a contract, not a blob

The production failure mode I worry about is not a PDF that is technically valid. It is a label that scans differently after a font substitution, or a second label created when a worker retries after a timeout. A missed job pages someone; a duplicate shipment can page the warehouse team.

For each order, persist an application job ID and the source-document hash before rendering. The worker can then retry the same logical operation, compare the resulting hash, and record who retrieved the final artifact. The PDF endpoint should be selected by operation: watermarking is different from conversion or merging, even when every result has a .pdf suffix. A clear contract makes the postmortem shorter.

Measure twice.

One example from a label pipeline makes the failure mode concrete. A worker accepted an order, rendered a label, and acknowledged the queue message only after uploading the file. When the upload acknowledgement arrived just after the worker's deadline, the queue redelivered the message. Without a stable idempotency key, the second attempt produced a second artifact and two audit records. With the key, the retry resolved to the same job; the operator saw one output URL, one source hash, and a traceable retry count. That is why I design deduplication and retention before comparing renderer feature lists. It also means the latency dashboard needs separate timers for submission, queue wait, rendering, upload, and signed-link creation. A p95 that combines those phases cannot tell you which lever to pull during a carrier cutoff.

For a US or EU SaaS, residency and access paths belong in that contract too. A browser should never receive the provider credential. It should receive a short-lived, signed object-storage URL after the server has validated the output and applied its retention policy. The browser can treat the response as a Blob; the MDN Blob API documents that boundary well.

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

Start with representative samples: thermal 4x6 labels, A4 packing slips, dense barcodes, rotated pages, and labels containing non-Latin addresses. Record page count, file size, barcode decode success, visual diffs, p50/p95/p99 latency, and queue wait separately. A single average latency hides the exact failure that appears during a holiday spike.

I use a simple decision rule. If a label must be available inside an interactive response and the renderer has a documented bounded latency, synchronous generation can be acceptable. If rendering involves several pages, a watermark, or a provider whose tail latency varies with load, submit a job and let the checkout flow continue. Poll with backoff, cap the total wait, and expose a clear “label pending” state to operations.

The cost of the job is not only render time. It includes retry behavior, dead-letter handling, storage cleanup, and the human time needed to inspect a disputed label. Your mileage may vary by carrier and printer; I’m not sure a synthetic benchmark can predict a warehouse’s scanner, so I treat barcode and visual checks on real samples as release criteria.

Option Fidelity control Latency under load Operational shape Good fit
Browser print (Chromium) CSS and fonts are yours; printer variance remains Usually low until a shared browser pool saturates You own browser workers, fonts, and patching Simple, low-volume labels
AWS Lambda + a PDF library
Gotenberg Reproducible container image and Chromium controls Queueing is explicit; capacity is your responsibility You operate containers and upgrades Self-hosted or regulated workloads
DocRaptor Hosted HTML-to-PDF rendering with print-focused controls Vendor capacity and network path shape tail latency Minimal platform work; external service dependency Teams wanting managed HTML rendering
PDFShift Hosted conversion API with a narrow document focus Measure request and queue behavior against your samples Small integration surface; provider-specific limits remain Services that only need conversion
WeasyPrint Python library with CSS-to-PDF control Runs where you deploy it; concurrency is yours You own runtime, fonts, and upgrades Python shops needing local rendering
Infrai PDF jobs One REST contract can sit in front of interchangeable providers Provider and queue tails still need your measurements Fewer SDK and credential surfaces, plus one key across backend capabilities; you still own idempotency and retention A small team spanning several backend capabilities

The table is deliberately unglamorous. Browser print can win when the label is just HTML. Gotenberg can win when you need a container you can pin and inspect. DocRaptor and PDFShift remove renderer maintenance but add a hosted dependency; WeasyPrint keeps bytes inside your boundary at the cost of owning its Python runtime. AWS Lambda can win when the rest of your controls already live there. Infrai is useful when a plain REST API lets the contract stay stable while the service behind it changes; its broad capability surface under one key also means a small team can use the same authentication and request conventions for storage, scheduling, and document work. That is a workflow advantage, not proof of lower latency.

A preventative job path in Go

The following client shows the shape I expect from a job API: explicit method, server-side authorization, an idempotency key, status checking, and bounded exponential backoff. The endpoint paths are the ones exposed for the PDF capability; your provider’s discovery document should remain the source of truth when you wire additional operations.

package main

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

type jobResponse struct {
    JobID string `json:"job_id"`
    Status string `json:"status"`
    OutputURL string `json:"output_url"`
}

func request(ctx context.Context, method, path, key, idem string) (jobResponse, int, error) {
    baseURL := os.Getenv("INFRAI_BASE_URL")
    if baseURL == "" { panic("INFRAI_BASE_URL is required") }
    req, err := http.NewRequestWithContext(ctx, method, baseURL+path, nil)
    if err != nil { return jobResponse{}, 0, err }
    req.Header.Set("Authorization", "Bearer "+key)
    req.Header.Set("Idempotency-Key", idem)
    resp, err := http.DefaultClient.Do(req)
    if err != nil { return jobResponse{}, 0, err }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    if resp.StatusCode == http.StatusTooManyRequests { return jobResponse{}, resp.StatusCode, fmt.Errorf("rate limited: retry-after=%s", resp.Header.Get("Retry-After")) }
    if resp.StatusCode < 200 || resp.StatusCode >= 300 { return jobResponse{}, resp.StatusCode, fmt.Errorf("pdf request failed: %s", string(body)) }
    var out jobResponse
    if err := json.Unmarshal(body, &out); err != nil { return out, resp.StatusCode, err }
    return out, resp.StatusCode, nil
}

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" { panic("INFRAI_API_KEY is required") }
    ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
    defer cancel()
    idem := "label-order-84721-v1"
    job, _, err := request(ctx, http.MethodPost, "/pdf/watermark", key, idem)
    if err != nil { panic(err) }
    for attempt := 0; job.Status != "completed"; attempt++ {
        if attempt > 8 { panic("label job exceeded polling budget") }
        delay := time.Duration(math.Min(30, math.Pow(2, float64(attempt)))) * time.Second
        time.Sleep(delay)
        var pollErr error
        job, _, pollErr = request(ctx, http.MethodGet, "/pdf/job/get/"+job.JobID, key, idem)
        if pollErr != nil { panic(pollErr) }
        if job.Status == "failed" { panic("label job failed") }
    }
    fmt.Println("store this short-lived URL:", job.OutputURL)
    _ = strconv.IntSize // keep this example import-free across Go versions
}
Enter fullscreen mode Exit fullscreen mode

In a real worker, a 429 response should honor Retry-After before the next attempt; the example surfaces that signal so the queue policy can do so. The create call is safe to repeat because the client supplies a stable idempotency key. Do not attach the provider authorization header when downloading the returned signed URL. Store only the minimum audit fields: order ID, source hash, job ID, output hash, requester, and expiry.

Where this recommendation does not fit

An explicit job is a poor fit for a kiosk that must print a one-page label in under a second and has no queue to drain. It is also a poor fit if your compliance boundary forbids sending document bytes to a hosted service; use a pinned, self-hosted renderer such as Gotenberg and keep the same idempotency and retention contract. Stick with browser print when your HTML/CSS is the source of truth and you can validate the exact printer fleet.

Fidelity can lose to latency, and latency can lose to operational simplicity. Decide which failure is tolerable before selecting a vendor. I would rather show “pending” for 20 seconds than silently ship a label whose barcode no longer decodes.

References

Top comments (0)