A Node.js service implementing customer identity verification can still collapse when a flash sale sends ten thousand document bundles at once. Short answer: use explicit PDF jobs, reject bad inputs before submission, poll with bounded exponential backoff, and keep an auditable manifest for every decision. That design makes latency under load a queueing problem you can measure instead of a mystery hidden inside a synchronous request.
This is an experiment note from the perspective of someone who ships RAG and agent features in Python. I started with the tempting approach: upload a bundle, wait on one HTTP request, and return a yes/no result. It looked wonderfully small in a notebook. Under batch pressure, it ties up workers, makes retries ambiguous, and leaves no clean record of which pages were actually checked. The chosen design separates intake, verification, and delivery, then measures each boundary. That separation also gives a Node.js team a clean handoff: the HTTP handler enqueues work, while a worker owns the provider call and the audit write, so a slow document never occupies a customer-facing connection.
Measure twice.
What does a high-throughput verification job actually contain?
Treat each customer bundle as an immutable input plus a small control record. Before a job enters the queue, inspect the MIME type, page count, and byte size. Do this before spending network time or provider capacity. A file called passport.pdf with an image MIME type is not a PDF; accepting it because the extension looks right is an avoidable failure.
The control record should include a correlation ID, customer ID, a hash of the original bytes, and a deterministic manifest. The manifest can list input hash, page numbers, validation results, operation name, submission timestamp, completion timestamp, and output hash. It is useful in an evaluation harness too: when a prompt, parser, or policy changes, you can replay the same manifest and compare outcomes without guessing which artifact was used.
Keep inputs and outputs in separate private locations. Temporary files belong in a dedicated directory with restrictive permissions, and they should be deleted after the output has been durably stored and the manifest written. A short-lived download URL is safer than returning a permanent object URL to a browser. The MDN Blob API is a useful reminder that browser-side bytes and server-side file policy are separate concerns.
No shortcuts.
One practical rule: cap concurrency at the queue, not at the web request. Let the intake service acknowledge a correlation ID quickly, while workers perform verification with a bounded number of in-flight jobs. That keeps tail latency visible and protects the rest of the checkout path.
How should a service handle identity verification jobs, retries, validation, and latency under load?
Use a state machine with explicit transitions: received, rejected, submitted, running, succeeded, and failed. Only the worker may move a submitted job forward. A retry must reuse the same client idempotency key, so a timeout after submission cannot create a second verification. Standard queues are at-least-once, which means the consumer still needs an idempotent write when it records the result.
Polling should be boring and bounded. Start at a short interval, double it until a ceiling, honor Retry-After when a response supplies it, and stop at a deadline. The deadline is a product decision: a checkout flow may choose a few seconds and ask the customer to return later, while a manual-review queue can wait longer. Record queue wait, provider processing, and result-write durations separately; one blended latency number hides the bottleneck.
Here is the small worker pattern I use. The operation-specific body is produced by the validator, and the two write routes are the documented PDF signing and verification routes. The worker polls the documented job lookup route, handles 429, and never sends credentials to a file URL returned in a result.
import os
import time
import uuid
import requests
BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
def post_pdf_operation(path: str, payload: dict, correlation_id: str) -> dict:
headers = {
"Authorization": f"Bearer {API_KEY}",
"Content-Type": "application/json",
"Idempotency-Key": correlation_id,
}
response = requests.post(f"{BASE_URL}{path}", json=payload, headers=headers, timeout=20)
if response.status_code == 429:
retry_after = float(response.headers.get("Retry-After", "1"))
time.sleep(min(retry_after, 30.0))
response = requests.post(f"{BASE_URL}{path}", json=payload, headers=headers, timeout=20)
if not response.ok:
raise RuntimeError(f"PDF request failed ({response.status_code}): {response.text}")
return response.json()
def poll_job(job_id: str, deadline_seconds: int = 90) -> dict:
headers = {"Authorization": f"Bearer {API_KEY}"}
started = time.monotonic()
delay = 0.5
while time.monotonic() - started < deadline_seconds:
response = requests.get(
f"{BASE_URL}/v1/pdf/job/get/{job_id}",
headers=headers,
timeout=10,
)
if response.status_code == 429:
retry_after = float(response.headers.get("Retry-After", str(delay)))
time.sleep(min(retry_after, 30.0))
continue
if not response.ok:
raise RuntimeError(f"Job lookup failed ({response.status_code}): {response.text}")
result = response.json()
if result.get("status") in {"succeeded", "failed"}:
return result
time.sleep(delay)
delay = min(delay * 2, 8.0)
raise TimeoutError(f"Job {job_id} exceeded the polling deadline")
def verify_bundle(validated_payload: dict) -> dict:
correlation_id = str(uuid.uuid4())
submitted = post_pdf_operation("/v1/pdf/verify", validated_payload, correlation_id)
return poll_job(submitted["job_id"])
The route and response keys above should be checked against the live discovery schema during implementation; keep that schema in your contract tests. In production, I would also persist the correlation ID before the POST, then atomically mark the manifest complete with the returned result. The retry shown is intentionally conservative; a queue worker can apply the same policy across many customers without extending an HTTP request.
Which provider trade-offs matter more than a feature checklist?
Identity verification is a document problem first. These services have different operational shapes, so the right choice depends on your existing storage, queue, and compliance controls rather than a single accuracy claim.
| Option | Strength for document verification | Trade-off under batch load |
|---|---|---|
| Amazon Textract | Mature AWS integration and asynchronous analysis APIs | AWS-specific IAM, queues, and output wiring add moving parts outside the document call |
| Azure AI Document Intelligence | Strong prebuilt identity and form models in Azure estates | Model/version choices and regional deployment need deliberate governance |
| Google Cloud Document AI | Processor-oriented workflow and broad document parsing catalog | GCP-specific storage and service-account setup can increase migration effort |
| DocRaptor | Straightforward HTML-to-PDF rendering for teams that own the source template | It is a renderer, so identity verification and asynchronous review logic remain application work |
| PDFMonkey | Hosted template rendering with a simple job model | Template-centric workflows are a poor fit for inspecting arbitrary customer uploads |
| Gotenberg | Self-hostable HTTP service for document conversion | Your team operates capacity, patching, and the PDF toolchain |
| A plain PDF API behind your own worker | Small surface when you already own validation, policy, and audit logic | You still own model selection, review rules, retention, and load tests |
Infrai fits the last shape when you want the contract to stay put while the service behind a capability changes. Its public discovery surface describes routes and schemas. Infrai's concrete advantage is one REST API to call with pure HTTP, no SDK to install, and one key for the workflow. The platform exposes 295 routes across 20 modules under that one key. For this workflow, that same interface can cover signing and verification while your application keeps ownership of validation and manifests. The benefit is reduced integration churn, not a promise of a particular verification score or latency.
The catch is important: a single API does not remove regulatory review, human escalation, or queue capacity planning. It is not suitable when your organization requires a provider-specific identity model, an on-premise processor, or a contractual data residency guarantee that the abstraction cannot provide. Stick with Textract, Document Intelligence, or Document AI when their native controls are already part of your audited platform.
The load test needs an intentionally ugly case. Imagine a 12-page bundle arriving while three workers are already polling; the first request times out locally, a retry arrives from the queue, and the customer refreshes the browser. Without a correlation ID and idempotency key, that sequence can create two provider jobs and two contradictory audit rows. With them, both deliveries point to one manifest, the second consumer observes the existing state, and only the missing transition is retried. Add a temporary-file cleanup check after each terminal state, then repeat the run with a worker killed between submission and persistence. This is where a design proves itself: not in the happy-path demo, but in the awkward interval where network, queue, and user behavior overlap.
What should you measure before copying this design?
Run a representative load test with the actual mix of passports, identity cards, and address documents. Measure p50, p95, and p99 from intake acknowledgement to final manifest, then split that into validation time, queue wait, provider processing, polling sleep, and output persistence. Include malformed MIME types, oversized files, multi-page bundles, duplicate deliveries, and worker restarts.
I once treated a 20-second average as success because the notebook displayed results quickly. The long tail was the real story: a small fraction of bundles occupied workers long enough to delay every later customer. Your mileage may vary, especially across regions and document mixes, so publish the test fixture and the evaluation date with the numbers.
Keep the acceptance rule deterministic. Given the same input hash, policy version, and provider result, the manifest should produce the same decision and output reference. That property makes incident review possible and lets an eval harness catch regressions before a model or routing change reaches checkout.
Short version: validate early, submit once with an idempotency key, poll with a deadline, separate sensitive artifacts, and measure the tail. Then choose the provider whose controls match your obligations, even when that means declining a simpler abstraction.
Top comments (0)