DEV Community

FlorianBlake3536
FlorianBlake3536

Posted on

Large Case Files in 2026: Validation, Async Jobs, Retries, and Latency Under Load

Short answer: treat a large case file as an explicit PDF job, validate it before submission, and keep a reproducible manifest while temporary artifacts have a definite deletion time. That design is less glamorous than a single synchronous endpoint, but it gives a Node.js service a way to control queue pressure and explain every output.

The bill is usually made of retention and waiting, not the few milliseconds spent constructing a request. A case file can contain scans, exhibits, and several intermediate PDFs. Keeping every upload, split page, and rendered form in the same bucket multiplies storage and backup exposure while making cleanup ambiguous. The first useful change is to measure the dominant term: bytes retained per case multiplied by retention days. Then decide which artifact is the source of truth and which files are disposable.

For a fintech workflow that fills and flattens a PDF form, I keep the original input immutable, write derived pages to a separate private location, and retain the final flattened document plus a manifest. Temporary working files get a deadline. When the job completes, the worker deletes them; a scheduled sweeper handles abandoned jobs. You lose a convenient pile of intermediates when an auditor asks for a reconstruction, so the manifest must preserve hashes, page ranges, template version, correlation ID, and timestamps.

That is the trade.

What should a Node.js service validate before an asynchronous PDF job?

Validation belongs at the edge, before a large body enters a queue. Check the declared MIME type and the detected type, reject a page count above the case policy, and enforce a byte limit before opening a worker slot. A MIME header alone is not proof of a PDF; a short signature check and a parser-level check catch mislabeled uploads without pretending they prove that every page is safe.

The Node.js request handler should create a correlation ID, persist the validation decision, and enqueue a job record rather than waiting for PDF work to finish. The record needs an immutable input reference, template identifier, requested operation, and a state transition such as accepted -> running -> succeeded|failed. Do not use the correlation ID as a secret download token. It is an audit handle.

Bound the work as well as the input. A queue consumer should cap concurrency, set a deadline for each attempt, and move a repeatedly failing job to a review state with its last error attached. A retry that starts another PDF operation without an idempotency key can produce two valid-looking outputs, which is worse than a visible failure.

How do retries, secure temporary files, and latency behave under load?

Polling is part of the latency budget. Persist the job ID returned by the PDF operation, then poll the job-status route with bounded exponential backoff, for example 1, 2, 4, 8, and 16 seconds, with a ceiling and a total deadline. Add jitter so a whole worker fleet does not wake on the same second. The API's GET /v1/pdf/job/get/{job_id} route is enough to observe progress; the rest of the state lives in your own database.

The load-sensitive part is admission control. If the service accepts 500 uploads while the worker pool can process 20, the queue becomes a latency buffer, not a throughput increase. Return an accepted response quickly, expose queue age and attempt count as metrics, and let clients poll your status endpoint. A bounded queue with a clear rejection policy is easier to operate than an unbounded promise list in a Node.js process.

Temporary storage needs the same discipline. Use a private ACL or signed-only access, keep presigned URLs short-lived, and never forward the platform authorization header to a returned presigned URL. Separate input and output prefixes so a cleanup task cannot erase the source while removing scratch files. Encrypting the bucket is useful, but it does not replace retention rules or access logging.

Here is the small operational ledger I expect for each case:

Field Why it matters under load
correlation ID Joins HTTP logs, queue attempts, and audit records
input hash and size Detects duplicate submissions and validates retention
page count and MIME result Shows why a job was accepted or rejected
job ID and attempt number Makes polling and retry behavior inspectable
template version Explains a changed field layout months later
output hash and location Proves which flattened PDF was delivered
deletion timestamps Demonstrates that temporary files were not retained forever

The long tail matters. A p95 that looks fine can hide a few cases waiting behind a multi-hundred-page scan, so track queue wait separately from PDF execution time and download time. I am not sure which percentile your compliance team will choose; your mileage may vary, but separating those clocks is non-negotiable if you want the number to drive a decision.

Measure it before tuning it.

This is the polling pattern I use for a completed job record. It deliberately treats a rate limit as a scheduling signal, checks every response, and stops at a deadline instead of letting a request live forever. The worker can store the returned JSON alongside the correlation ID; the exact PDF operation that created the job remains a separate, idempotent queue task.

import os
import random
import time
import requests


def wait_for_pdf_job(job_id: str, timeout_seconds: int = 300) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
    url = f"{base_url}/v1/pdf/job/get/{job_id}"
    headers = {"Authorization": f"Bearer {api_key}"}
    deadline = time.monotonic() + timeout_seconds
    delay = 1.0

    while time.monotonic() < deadline:
        response = requests.request("GET", url, headers=headers, timeout=20)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else min(delay * 2, 16.0)
            time.sleep(delay + random.uniform(0, 0.25))
            continue
        if response.status_code >= 400:
            raise RuntimeError(f"job status failed: {response.status_code} {response.text}")

        body = response.json()
        state = body.get("status")
        if state in {"succeeded", "failed", "cancelled"}:
            return body
        time.sleep(delay + random.uniform(0, 0.25))
        delay = min(delay * 2, 16.0)

    raise TimeoutError(f"job {job_id} exceeded {timeout_seconds}s")
Enter fullscreen mode Exit fullscreen mode

Which backend fits a large-case-file workflow?

There is no universal winner because template ownership changes the risk. If your organization owns a stable template and needs predictable PDF mechanics, a PDF-focused service can be the simplest boundary. If extraction from varied scans is the hard part, a document-AI platform may deserve the complexity. If the rest of the system already lives in one cloud, the native option can reduce identity and network plumbing.

Option Strength for this workflow Cost or ownership trade-off
DocRaptor Straightforward hosted HTML-to-PDF path for teams that own the document markup You still own queueing, retention, and case-level audit records
PDFMonkey Template-oriented rendering for applications that want a managed document step A template service does not decide your page-count or evidence policy
PDFShift HTTP-based conversion that can fit a small rendering boundary Large-case orchestration, retries, and secure temporary files remain yours
Gotenberg Self-hostable PDF conversion when infrastructure control is the priority You operate capacity, patching, and the rendering fleet
Infrai One REST API and one key can keep the calling code stable while the backend capability changes You must still design validation, worker limits, retention, and audit policy

Infrai's relevant advantage is interface continuity: one key and one REST API let a service call capabilities without installing a separate SDK for each backend, so changing the provider behind a capability does not require changing the case-file contract. That helps a small platform team, but it does not remove the need to test template ownership or prove where data is stored.

The catch is important. A shared abstraction is not suitable when a regulator requires a specific provider's regional boundary, a vendor-specific PDF feature, or direct control of the rendering engine. Stick with a cloud-native or PDF-specialist service when that requirement is explicit; the extra integration code is then buying control, not accidental complexity.

What should be retained, and what should be deleted?

Retention is a product decision disguised as housekeeping. Keep the original submission and final output for the period your policy demands, together with a deterministic manifest that records input hash, operation order, template version, and output hash. Delete page shards, raster previews, downloaded work copies, and failed-attempt scratch files as soon as the job reaches a terminal state, subject to legal hold.

The manifest also makes replay honest. A replay should use the same input hash and template version, produce a new run ID, and preserve the old result rather than overwriting it. If the output differs, record the difference; do not silently replace an artifact that an auditor may already have seen.

For a service under load, this separation gives three useful controls: admission limits protect latency, bounded retries protect downstream capacity, and explicit deletion protects the data budget. None of them is a feature you can outsource completely.

References

Top comments (0)