DEV Community

KillianBerg5391
KillianBerg5391

Posted on

Receipts and Expense Reports: Hosted PDF APIs vs Local Libraries at 10k-Request Loads

Short answer: a hosted PDF API is preferable when a team needs one owned template, predictable operational work, and a way to absorb bursts without maintaining a rendering fleet. Local PDF libraries win when data must stay inside the network, the layout engine needs deep customization, or a dependency call would break the latency budget. At production scale, measure queue time, render time, transfer time, and failure recovery separately; a single average latency hides the decision.

For healthtech receipts and expense reports, template ownership is the first fork. The invoice or receipt is a regulated record, not a decorative download. Someone needs to review the source template, approve a version, and explain why a patient-facing total changed. The rendering location matters because it defines where order data, tax fields, and generated bytes travel.

The decision starts with template ownership

A local library gives the application direct control over the template and its dependencies. That is useful when designers and engineers iterate on typography, page breaks, embedded fonts, or a custom barcode. The application can pin the renderer, run it in the same build, and keep the input data within a private boundary. The cost is ownership of native packages, font files, memory limits, and a test matrix for every runtime update.

A hosted API moves the renderer outside that application boundary. The team owns the request contract and the template version, while the service owns the worker process, sandbox, and capacity plan. That can be a good exchange for a small platform team, especially when receipts arrive in irregular waves after a reimbursement cycle. It is a poor fit when policy requires every render to happen in an isolated network or when an offline workflow is mandatory.

The catch is governance. A hosted design needs explicit data-retention terms, regional routing, encryption controls, and a deletion story for both source payloads and output blobs. A local design needs equally explicit controls for logs, temporary files, and crash dumps; “inside our VPC” does not automatically mean “never persisted.”

When is a hosted PDF API preferable to local libraries?

Think in a queue, not a stopwatch. At 10k requests, a renderer that takes 180 ms in an idle benchmark can still produce multi-second tail latency if workers saturate or if a downstream connection pool is too small. Record p50, p95, and p99 for each stage: admission, queue wait, template fetch, rendering, upload, and the client download. Set a deadline for the whole job and shorter budgets for each stage.

Measure the tail.

A hosted API usually adds network round trips and serialization. In return, it can isolate CPU-heavy rendering from the order service and scale workers independently. A local library avoids network transfer for the render itself, but its process competes for the same CPU and memory as request handling unless you put it behind a queue. Neither choice guarantees low tail latency. Capacity and backpressure do.

Here is a small measurement harness for a generic HTTP endpoint. It treats the response as bytes, which is the useful contract for a receipt download, and it keeps the timing fields separate so an eval run can catch regressions.

from dataclasses import dataclass
from time import perf_counter
from typing import Any

import requests


@dataclass
class RenderTiming:
    total_ms: float
    status: int
    size_bytes: int


def render_receipt(endpoint: str, order: dict[str, Any], timeout_s: float = 5.0) -> RenderTiming:
    started = perf_counter()
    response = requests.post(endpoint, json=order, timeout=timeout_s)
    response.raise_for_status()
    elapsed = (perf_counter() - started) * 1000
    return RenderTiming(elapsed, response.status_code, len(response.content))
Enter fullscreen mode Exit fullscreen mode

The harness is intentionally boring. Feed it fixed orders, large orders, missing optional fields, and bursts that match the real reimbursement calendar. Your mileage may vary: network distance, template complexity, and font loading can dominate different stages.

Concern Hosted PDF API Local PDF library
Template control Versioned through an external service contract Fully controlled in the application build
Load isolation Renderer workers can scale apart from order handling Requires a separate worker pool to avoid contention
Data boundary Payload crosses a service boundary Payload can remain inside the private network
Operations Less native-package maintenance, more dependency governance More patching and capacity work, fewer network hops

What fails first in receipts and expense reports?

The visible PDF is often the last symptom. A template change can shift a total onto a second page; a missing currency code can make a correct number ambiguous; a retry can create two receipt IDs for one order. Make the job idempotent with a stable order-and-template key, persist a render status, and publish the download only after the bytes pass validation.

One duplicate is enough to trigger a reconciliation ticket.

Treat the PDF as an artifact with metadata: template version, renderer version, input hash, creation time, and a checksum. Store those fields beside the object rather than relying on a filename. For browser delivery, return a Blob-compatible byte response and set an explicit media type; the web platform defines Blob as immutable, byte-oriented data.

Observability should expose correlation IDs without placing patient or employee names in logs. Capture structured error classes such as timeout, rejected input, and validation failure. Retry only transient transport failures, with bounded exponential backoff and a queue limit. A retry that ignores idempotency is a billing incident waiting to happen.

A practical production scorecard

Before choosing, run the same corpus through both designs. Include the shortest receipt, the longest expense report, images at their maximum accepted size, and a template revision that adds a page. Compare tail latency under a sustained rate and a burst, memory per worker, cold-start behavior, deployment frequency, and the time required to roll back a template.

For example, imagine a reimbursement job that releases 10,000 receipts over five minutes. The order service accepts work at a steady rate, but the renderer sees a burst when a batch is approved. With a local library in the web process, CPU saturation delays unrelated order lookups, so the apparent PDF latency includes contention that the idle benchmark never measured. With a hosted API, the order service stays responsive, yet queue wait grows if the remote worker pool reaches its limit; the client may see a fast connection followed by a slow completion. Instrument both queues, cap in-flight jobs, and shed optional preview renders before final receipts. That experiment tells you which boundary protects the patient workflow, which one protects the template team, and where a retry would multiply work.

Then score the constraints that are hard to change: data residency, audit retention, offline operation, language support, and who can approve templates. A hosted service is not suitable when its region or retention model conflicts with policy. A local library is not suitable when the team cannot staff patching, font licensing, and capacity testing. Stick with the option that makes the riskiest constraint boring, even if its median benchmark looks less impressive.

The recommendation should come from those measurements, not from a vendor comparison or a price sheet. Keep a small canary corpus in CI, alert on p95 and p99 rather than only averages, and repeat the load test after every renderer or template change.

References

Further reading

Top comments (0)