nilsberg2187Short answer: use an endpoint contract that preserves immutable PDF bytes and artifact identity, then...
Short answer: use an endpoint contract that preserves immutable PDF bytes and artifact identity, then put high-volume work behind a bounded batch queue. That choice usually gives a US/EU SaaS better latency under load than making every caller wait for rendering, while still leaving fidelity testable and operational work visible.
I learned this while operating a B2B SaaS workflow that filled and flattened PDF forms for compliance evidence. A queue drained normally until a large batch arrived near a reporting deadline. Some jobs finished quickly; others sat behind font loading and downstream retries. A second delivery attempt then created duplicate evidence records. The page looked fine.
The ledger was wrong.
The invariant is simple: treat every generated PDF as an artifact with an identity, not as a transient HTTP response. Store a content digest, template revision, input revision, and creation timestamp beside the bytes. That record makes a slow job diagnosable and a replay safe. It also gives an auditor a concrete answer when the same form is requested twice months apart.
Start with a contract that is boring to operate. A request should identify the template and input revision, declare whether the caller wants a single result or a batch, and carry an idempotency key. A successful response should include the PDF bytes or a durable object reference plus the digest and status metadata. A retry with the same key must resolve to the same artifact identity.
The browser-facing detail matters too. The MDN Blob API describes a Blob as immutable, file-like raw data that can be read or handed to APIs expecting binary data. That is a useful boundary for a web client: keep the response as bytes, set an explicit media type, and avoid silently converting a PDF into text. In a service, the equivalent rule is to preserve bytes from renderer to object storage.
For US/EU SaaS, evidence metadata should make retention and access review possible without putting sensitive form fields into logs. Log identifiers, sizes, durations, and outcome codes. Keep the filled content in the controlled evidence store, with an access policy that matches the compliance program. This is an operational design choice, not a promise that one endpoint makes the workload compliant.
Use two paths with one artifact contract. A synchronous path is appropriate for a single interactive form when the p95 budget is known and the caller can tolerate a bounded timeout. A batch path should accept work, return a job identifier, and let workers flatten forms with a concurrency limit. The limit protects the renderer and makes queue delay visible instead of turning load into a retry storm.
Fidelity has a cost. Fonts, form-field appearance, page boxes, and annotation flattening can change the rendered bytes even when the source data is identical. Define a small fixture set that includes long names, missing optional fields, accented characters, and multi-page forms. Compare output hashes for exact-repeat tests, and use rendered-page review when visual fidelity is the requirement. Do not use a single latency number as a proxy for either test.
A practical worker keeps the state transition narrow:
package evidence
import "context"
type EvidenceJob struct {
ID string
IdempotencyKey string
TemplateRev string
InputRev string
}
func process(ctx context.Context, job EvidenceJob) error {
artifact, err := renderAndFlatten(ctx, job.TemplateRev, job.InputRev)
if err != nil {
return err
}
// Commit bytes and metadata together; a retry observes the same identity.
return commitArtifact(ctx, job.IdempotencyKey, artifact)
}
The exact renderer is replaceable. The queue, idempotency store, and artifact record are the controls that keep a missed or duplicated delivery from becoming an evidence incident. My runbook pages the team on queue age, oldest job age, renderer duration, retry count, and duplicate-key conflicts. Those signals separate capacity pressure from malformed input.
The catch is that asynchronous processing adds state and a second read. It is not suitable when a user must download the file in the same request and cannot poll or receive a callback. Stick with a bounded synchronous endpoint for that narrow interaction, and enforce a small payload and timeout budget.
There are three common shapes. A direct byte response is easy for a caller and hard to protect under bursts. A presigned object reference separates rendering from download but requires expiry and authorization checks. A job endpoint supports batching and backpressure, at the cost of status storage and cleanup. None is universally best.
| Endpoint shape | Strength under load | Operational cost |
|---|---|---|
| Direct PDF bytes | One request, immediate delivery | Long connections and burst-sensitive workers |
| Object reference | Download can be retried separately | Expiry, authorization, and retention checks |
| Job plus status | Queueing and concurrency are explicit | State storage, polling or callbacks, and cleanup |
For batch throughput, measure completed artifacts per minute while recording queue wait separately from render time. Run the test at the concurrency you can actually operate, then repeat it with a deliberately slow input. A system that wins a quiet benchmark but collapses under a queue of 10,000 forms has not met the decision axis. Your mileage may vary because font packs, template complexity, and storage locality change the shape of the curve.
Operational complexity is a budget. Every extra state, callback, or retention rule needs an owner, a dashboard, and a recovery procedure.
If the team cannot explain how to replay one failed job without duplicating an evidence record, the endpoint is underspecified regardless of its fidelity score.
Choose the smallest contract that preserves artifact identity, byte fidelity, and an auditable lifecycle. For interactive completion, use synchronous rendering with a strict deadline. For compliance exports and large form batches, enqueue jobs, cap concurrency, and expose queue age alongside render latency. Test exact repeats and visual fixtures before changing renderers.
Then write down the boundary. The recommended batch design is a poor fit for a tiny, low-volume workflow where operating a queue costs more attention than the PDFs save. In that case, a single process with durable storage and a narrow synchronous API is easier to reason about.
This is the postmortem lesson I keep: latency is a property of the whole path, while fidelity is a property of the artifact. Design and measure both, or the dashboard will tell you only half of the story.