DEV Community

KiernanBerg3867
KiernanBerg3867

Posted on

PDF Endpoint Governance Explained: Fair Branded SaaS Delivery Under US/EU Load

A US/EU SaaS should use separate PDF endpoints for branded document delivery because batch throughput changes the latency decision: a merchant's 40-page supplier scan cannot delay another merchant's one-page packing slip.

Short answer: expose a bounded synchronous proof endpoint for human review, an asynchronous delivery endpoint for final documents, and a status endpoint for recovery; then govern all three with regional placement, per-tenant admission, and versioned brand assets rather than sending every request into one shared render queue.

This is a policy choice before it is a rendering choice. OCR introduces variable work, branded output makes visual drift visible, and US/EU operation adds a placement decision for source scans and generated files. A fast empty-system demo answers none of those problems.

There is a smaller option. When documents are fixed, tiny, and rare, one synchronous request can be the honest design. The extra queue, worker state, and cleanup described below are not suitable for that workload.

How should a US/EU SaaS govern PDF endpoints for branded delivery under load?

Start with four logical operations, even if an internal gateway later combines some of them. Intake records an immutable scan reference and its region. Proof returns a bounded PDF to a person checking branding. Delivery accepts a normalized document reference, brand revision, region, and idempotency key, then returns a job identifier. Status reports the job state and, after completion, an access-controlled output reference.

Operation Completion boundary Admission rule Why it exists
Intake Input reference recorded Region and size known Keeps upload time away from rendering
Proof PDF bytes returned Small, single-document budget Supports an interactive brand check
Delivery Job identifier returned Tenant has queue capacity Makes variable work retryable
Status Current job state returned Cheap read budget Supports polling and recovery

The proof endpoint is deliberately narrow. It shouldn't download arbitrary assets, perform a batch OCR run, or accept an unbounded record set. Its latency budget belongs to the merchant who is looking at the screen. Final delivery has a different completion boundary: acceptance means the work is durable, not that the PDF already exists.

Don't hide that distinction.

The status read matters even when completion events exist. Consumers need a way to reconcile their own records after a missed event or a restart. The application contract should describe states and identifiers, while an adapter owns authentication and provider-specific paths. That separation also keeps a migration from leaking into checkout, returns, and document-search code.

Why should tenant fairness outrank raw worker utilization?

A single first-in, first-out queue looks efficient until one importer submits thousands of OCR-derived catalogs. The queue may keep every worker busy while interactive proofs wait behind bulk jobs. Average latency can still look acceptable because the many short documents dilute a few painful waits. This is the wrong optimization for a multi-tenant product.

Classify admitted work by tenant, urgency, region, and document class. Give proof work a small reserved capacity pool. For final delivery, rotate across tenants and cap each tenant's in-flight jobs. Within a tenant, let short known classes progress without allowing large documents to starve forever. The exact weights depend on observed traffic; I'm not sure a static ratio will survive a seasonal import surge, so the ratio should be a deployable policy with queue-age alerts, not a constant buried in worker code.

One practical load experiment uses synthetic classes of 1, 8, and 40 pages, then increases the offered arrival rate while holding the worker pool fixed. Run at least two tenants: one steady producer and one burst producer. The result to inspect is not a heroic requests-per-second peak. Look for whether the steady tenant's proof latency remains bounded, whether the oldest accepted delivery keeps advancing, and whether admission closes before process memory becomes the queue. Those are experiment settings and pass conditions, not performance claims.

Overload must be visible.

Once the admitted queue reaches its configured limit, reject or defer new delivery work with an explicit retry signal. A producer that receives that signal should pause and resubmit with the same idempotency key. Silently buffering more work only moves the overload boundary into memory, where it is harder to observe and easier to lose. I'd rather explain a controlled refusal in an integration guide than explain why a small tenant waited behind a bulk import with no declared limit.

This TypeScript boundary keeps policy in the application without inventing a public URL scheme:

type Region = "us" | "eu";
type WorkClass = "proof" | "delivery";
type JobState = "accepted" | "running" | "complete" | "rejected";

interface DocumentIntent {
  tenantId: string;
  sourceRef: string;
  brandRevision: string;
  region: Region;
  workClass: WorkClass;
  idempotencyKey: string;
}

interface DocumentJob {
  id: string;
  state: JobState;
  outputRef?: string;
  retryAfterMs?: number;
}

interface DocumentGateway {
  createProof(intent: Omit<DocumentIntent, "workClass">): Promise<Blob>;
  submitDelivery(intent: DocumentIntent): Promise<DocumentJob>;
  readStatus(jobId: string): Promise<DocumentJob>;
}

interface AdmissionSnapshot {
  tenantInFlight: number;
  tenantLimit: number;
  regionalQueueDepth: number;
  regionalQueueLimit: number;
}

function canAdmit(snapshot: AdmissionSnapshot): boolean {
  return (
    snapshot.tenantInFlight < snapshot.tenantLimit &&
    snapshot.regionalQueueDepth < snapshot.regionalQueueLimit
  );
}
Enter fullscreen mode Exit fullscreen mode

The browser-facing proof returns a Blob, an immutable raw-data object described by MDN. That is a clean binary boundary for preview or download and avoids inflating the PDF into a base64 string. A browser object URL is local and temporary, however; revoke it after use and keep durable delivery behind an access-controlled object reference.

The code does not prescribe scheduler weights. Good. Those values should come from a load test using the real mix of scans, not from an attractive sample repository.

Treat brand fidelity as a governed release

Branding failures often arrive as ordinary template edits: a font revision changes a line break, a longer product name pushes totals onto another page, or OCR text preserves an unexpected line boundary. A release corpus should therefore include transparent logos, long names, missing optional fields, right-to-left text, multi-page tables, and uncertain OCR output. Pin the template and asset revision used for each expected artifact.

Visual comparison is only one gate. Extract text to check that a searchable document remains searchable, verify required phrases and links, record page count and output size, and retain the original scan according to its own policy. A PDF that looks right but loses searchable text has failed the stated e-commerce job.

Remote asset retrieval also belongs in policy. Fetching fonts or logos during rendering adds I/O to the critical path and lets an asset change without a template release. Package approved assets with the template when licensing permits, reject unknown revisions before admission, and make the exact asset set part of the job intent. This costs storage and release discipline, but it buys repeatability. For a storefront whose layout depends on complex browser behavior, only its real fixture corpus can establish fidelity; a generic feature list can't settle that choice.

Operational complexity is state you choose to own

An asynchronous delivery contract adds job records, retries, expiry, reconciliation, and worker deployment. It also creates a place to enforce tenant and regional policy before expensive work starts. The catch is straightforward: a team without bursty or variable work may pay that operational cost without receiving much benefit.

Use a direct, synchronous library when layouts are fixed, inputs are tightly bounded, and the calling process can afford the work. Use isolated browser-style rendering when fixture tests show that web-layout behavior is necessary, accepting its heavier worker footprint. Use an asynchronous managed or self-hosted service when delivery must survive caller timeouts and bursts. None is a universal winner, and price alone would be a poor deciding axis because queue behavior, artifact fidelity, and operational ownership dominate the failure modes here.

Regional handling needs equally explicit state. Store the selected region on the input, job, logs, and output metadata; route workers from that value; and test that a retry cannot silently cross the boundary. Keep document text and access-bearing output references out of routine logs. Define separate retention periods for source scans, extracted text, job metadata, and completed PDFs because they serve different purposes.

The uncomfortable part is deletion. A job can expire while an output object remains, or an output can be removed while a searchable-text index still contains extracted content. Test deletion as a multi-stage workflow with reconciliation, rather than treating one successful object deletion as proof that the document is gone everywhere.

Measure the policy before copying the architecture

Record offered load beside every result. Measure proof latency at p50, p95, and p99; delivery queue age; per-tenant wait time; OCR duration; render duration; admission refusals; retries; duplicate suppression; worker memory; output bytes; and completion rate by page class and region. A throughput number without queue age can describe a system that is falling further behind each minute.

Then test the ugly transitions — a cold worker pool, a burst from one tenant, a brand revision during queued work, a repeated idempotency key, and deletion of an expired job. The decision rule is simple: keep the bounded proof path only for a person waiting, put variable final delivery behind durable admission, and add fairness controls before batch OCR traffic reaches shared workers.

Copy this architecture only when the test shows stable queue age, bounded memory, tenant isolation, correct regional placement, and artifact fidelity on the real corpus. Otherwise, reduce the state you own or change the rendering boundary. Empty-system speed is easy. Fair behavior under load is the product.

References

Top comments (0)