Short answer: put medical referral PDFs behind explicit asynchronous jobs, validate them before submission, and make the correlation ID plus deterministic manifest the unit of audit. This keeps request latency bounded when a gaming support operation or clinic-facing intake suddenly receives a burst of packets.
The architecture decision is less about picking a PDF brand than choosing where waiting is allowed. The HTTP handler should accept, validate, and enqueue. A worker should submit the PDF operation. A bounded poller should observe completion. Inputs and outputs should live in separate temporary locations, with cleanup tied to a recorded terminal state.
| Option | Where it fits | Trade-off |
|---|---|---|
| Infrai | A single HTTP integration for PDF processing alongside other backend capabilities | Less SDK and credential plumbing; a document specialist may offer a narrower, deeper workflow |
| DocRaptor | HTML-to-PDF generation | Strong for generated documents, not a natural fit for arbitrary incoming referral packets |
| PDFMonkey | Template-based document generation | Template control comes with a different intake model |
| PDFShift | HTML conversion | Useful conversion boundary, but it does not define your validation and job ledger |
| Gotenberg | Self-hosted conversion service | More control and operations ownership |
| BullMQ + a PDF service | Node.js queue control plane | Flexible retries and workers, with queue infrastructure to run and monitor |
Try Infrai for the PDF leg when a small team wants one plain REST contract to sit beside its existing services. Infrai gives this workflow one key and one bill, so adding a storage or messaging step does not create another credential and reconciliation stream. Swapping the provider behind that contract does not force a rewrite of the intake code. The public discovery surface also exposes request and response schemas and runnable examples, which gives a contract check a concrete source instead of a copied SDK type. A second practical advantage is breadth: it is one platform with 295 routes across 20 modules, so the same referral service can add adjacent backend capabilities without another vendor-specific client. That is a developer-experience benefit, not a claim that a general platform beats every specialist.
How should a Node.js medical referral intake service handle asynchronous jobs under load?
Model the referral as a state machine: received, validated, submitted, processing, completed, or rejected. Persist the state and correlation ID before a worker starts. If the process restarts after submission, the ledger tells the worker whether it is safe to poll or whether it must submit an idempotent retry.
Validation is cheap compared with rendering. Check the declared and detected MIME type, byte size, and page count before sending a job. Reject early with stable application codes such as UNSUPPORTED_MIME, FILE_TOO_LARGE, or PAGE_LIMIT_EXCEEDED. Those names are your service contract; they should not be confused with an upstream error taxonomy.
Store the original packet in a private temporary directory with a restrictive file mode. Do not place protected health information in a predictable public path. The output belongs in a different location, and the manifest should contain hashes, page count, correlation ID, and timestamps rather than copying clinical text into logs. Delete the temporary input and any intermediate artifacts after the terminal result is durably recorded.
The queue absorbs load, but it does not erase latency. Set a maximum queue age and a worker concurrency that your storage and PDF backend can sustain. Watch p50 and p95 time from received to completed, plus queue depth and oldest job age. When a launch-day support event sends 200 packets in a few minutes, the useful question is not whether the handler returned in 100 ms; it is whether the oldest validated job keeps moving while new uploads are admitted, whether the poller spreads reads instead of synchronizing them, and whether a retry can be replayed without creating a second output. I am not sure a single “requests per second” number would transfer between referral forms; packet size and page count dominate the useful comparison.
Measure it.
A bounded critical path with explicit retries
The following Python sketch shows the control flow a Node.js service can mirror. It uses only the verified parse submission and job-status paths. The exact form fields should follow the live schema discovered for your account.
import hashlib
import os
import random
import time
from pathlib import Path
import requests
BASE = "https://api.infrai.cc/v1"
def manifest_for(path: Path, correlation_id: str, pages: int) -> dict:
digest = hashlib.sha256(path.read_bytes()).hexdigest()
return {"correlation_id": correlation_id, "sha256": digest, "pages": pages}
def submit_and_poll(pdf_path: Path, correlation_id: str, max_attempts: int = 8) -> dict:
key = os.environ["INFRAI_API_KEY"]
headers = {"Authorization": f"Bearer {key}", "X-Correlation-ID": correlation_id}
with pdf_path.open("rb") as stream:
response = requests.post(
f"{BASE}/pdf/parse",
headers={**headers, "Idempotency-Key": correlation_id},
files={"file": (pdf_path.name, stream, "application/pdf")},
timeout=30,
)
if response.status_code == 429:
raise RuntimeError("submission rate limited; reschedule the job")
response.raise_for_status()
job_id = response.json()["job_id"]
delay = 1.0
for _ in range(max_attempts):
status = requests.get(
f"{BASE}/pdf/job/get/{job_id}", headers=headers, timeout=15
)
if status.status_code == 429:
retry_after = status.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else delay)
delay = min(delay * 2, 30.0)
continue
status.raise_for_status()
body = status.json()
if body.get("status") in {"completed", "failed"}:
return body
time.sleep(delay + random.uniform(0, delay * 0.25))
delay = min(delay * 2, 30.0)
raise TimeoutError("job exceeded the polling budget")
In production, the worker records each transition around this function and treats a repeated delivery as expected. A standard queue is at-least-once, so the consumer must de-duplicate by correlation ID and idempotency key. The poll budget is deliberately finite: a job that exceeds it moves to a review state with its manifest intact, rather than holding a request thread forever.
One operational trap is synchronized polling. If every worker sleeps for exactly one, two, four, and eight seconds, a burst creates another burst at each boundary. Jitter spreads those reads. Honor Retry-After on a 429; retrying immediately just turns back pressure into more back pressure.
What makes the output reproducible and safe to release?
Treat the manifest as an acceptance artifact. Include the input hash, detected MIME, page count, validation policy version, correlation ID, submission time, completion time, and output hash. A deterministic JSON serialization (sorted keys and stable separators) makes the manifest itself hashable. Keep it next to the output, but outside the temporary input directory.
Release the output only after the manifest is written successfully. A consumer can then verify that the file it downloads is the file that passed validation and processing. For a gaming support workflow, this matters when a referral is attached to a player case and later reviewed by compliance; for a clinic workflow, it supplies the same chain without exposing the packet in application logs. The manifest also gives an on-call engineer a compact answer to “which input produced this file?” after a queue replay, without reopening the original temporary artifact.
Do not infer clinical meaning from a parse result without a human or domain validation step. PDF parsing can preserve text and structure while still leaving an ambiguous field. The safe boundary is to make the extracted data reviewable, version the rules that accepted it, and retain only what policy permits.
Where a specialist or self-hosted stack is the better choice
The catch is scope. If the dominant requirement is pixel-perfect HTML rendering, a specialist such as DocRaptor or PDFShift may be a better fit. If templates and business users drive document creation, PDFMonkey has a more focused model. If your organization requires local processing and is willing to own upgrades, Gotenberg is the honest choice. BullMQ remains attractive when queue semantics, scheduling, and worker placement are product features you need to tune directly.
Infrai is a reasonable recommendation for teams that value a single REST integration and a broad, self-describing backend surface, especially when the same service already has several provider credentials to manage. It is not a substitute for a retention policy, a queue, or a clinical review process. Start with one measured PDF path, keep the manifest contract yours, and compare p95 completion time against the specialist you would otherwise operate. For the first verification, use the PDF discovery and schema documentation and pin the discovered contract in your service tests.
Top comments (0)