Short answer: put every referral PDF behind an explicit asynchronous job, reject unsafe input before submission, and make polling plus cleanup part of the workflow contract. This keeps latency predictable under load and leaves an audit trail when a referral is questioned later.
The same pattern works for a developer-tools team that generates invoice PDFs from order data. A referral packet and an invoice are both untrusted documents moving through a finite worker pool; the failure modes are familiar: oversized payloads, duplicate deliveries, and a queue that looks healthy while work quietly ages.
How can a Node service implement medical referral intake under load?
Start with a correlation ID created at intake. Persist it with the order or referral record before any network call. Validate MIME type, page count, and byte size locally. A file named referral.pdf is not evidence that the bytes are a PDF, and accepting a 200-page scan into a small worker pool is a scheduling decision disguised as validation.
After validation, enqueue a job and return a receipt to the caller. The HTTP request should not wait for parsing. A worker owns the job, records each state transition, and writes output to a location separate from the original upload. Temporary files are deleted after the output and manifest have been durably recorded. Keep the retention rule explicit; “we clean it up later” is how protected health information ends up in /tmp for weeks.
Latency under load is mostly queueing latency. Track time from receipt to enqueue, queue wait, processing time, and poll delay separately. An SRE can then tell whether to add workers, cap intake, or tune polling instead of blindly increasing a client timeout.
Measure the handoff.
For invoice generation from order data, a month-end burst can fill the same worker pool used by referrals. A per-tenant concurrency limit, a bounded queue, and backpressure at intake keep that burst from turning into unbounded memory use. The limit should be visible in metrics and configuration, because an undocumented cap becomes a surprise during an incident. Keep a small priority lane for urgent clinical referrals only if the policy is approved and auditable; otherwise, FIFO is easier to reason about and replay.
How do validation and bounded retries protect asynchronous PDF jobs?
Use a deterministic manifest before submission. Include the correlation ID, content hash, byte size, page count, validator version, and an ordered list of requested operations. Store the manifest with the result. If a retry occurs, the worker can compare the same manifest and avoid creating a second logical result.
Polling needs a deadline, not an open-ended loop. Begin with a short delay, apply exponential backoff with jitter, honor Retry-After when present, and stop at a wall-clock limit appropriate for your intake SLA. A 429 is a scheduling signal. Backing off protects both the service and your own worker pool.
The write side must be idempotent. Use the correlation ID as the client-supplied idempotency key where the receiving API supports it, and make the database commit conditional on a manifest hash. Standard queues are at-least-once systems, so the consumer must tolerate the same message twice. This is not optional bookkeeping; it is the mechanism that prevents duplicate referrals or duplicate invoices.
A small worker loop with explicit PDF routes
The following Go sketch keeps the network boundary visible. The surrounding Node.js service can enqueue the same work, while a Go worker handles the bounded poll. It sends the bearer token only to the API host and treats non-success responses as actionable errors.
package main
import (
"context"
"fmt"
"io"
"math/rand"
"net/http"
"os"
"path/filepath"
"time"
)
func call(ctx context.Context, method, path string, body io.Reader) (*http.Response, error) {
req, err := http.NewRequestWithContext(ctx, method, os.Getenv("INFRAI_BASE_URL")+path, body)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
req.Header.Set("Idempotency-Key", os.Getenv("REFERRAL_CORRELATION_ID"))
return http.DefaultClient.Do(req)
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
defer cancel()
input := filepath.Join(os.TempDir(), "referral-source.pdf")
f, err := os.Open(input)
if err != nil { panic(err) }
defer f.Close()
resp, err := call(ctx, http.MethodPost, "/pdf/parse", f)
if err != nil { panic(err) }
if resp.StatusCode < 200 || resp.StatusCode >= 300 { panic(fmt.Errorf("parse returned %s", resp.Status)) }
resp.Body.Close()
delay := time.Second
for attempt := 0; attempt < 8; attempt++ {
time.Sleep(delay + time.Duration(rand.Int63n(int64(delay/3+1))))
status, err := call(ctx, http.MethodGet, "/pdf/job/get/"+os.Getenv("PDF_JOB_ID"), nil)
if err != nil { panic(err) }
if status.StatusCode == http.StatusTooManyRequests { status.Body.Close(); delay *= 2; continue }
if status.StatusCode < 200 || status.StatusCode >= 300 { panic(fmt.Errorf("status returned %s", status.Status)) }
// Decode the provider's job state here; commit output and manifest only when complete.
status.Body.Close()
delay *= 2
}
}
This sample deliberately leaves the response schema to the provider contract rather than inventing fields. In production, persist the returned job identifier with the correlation record, cap the number of attempts, and move a timed-out job to a review queue. I’m not sure a single fixed timeout fits every referral mix; your mileage will vary with scan quality and page counts, so derive it from observed queue-age percentiles.
Which service fits the operational boundary?
No platform removes the need for local validation, idempotent consumers, or PHI controls. The useful comparison is how much orchestration your team must own.
| Option | Async and retry posture | Integration shape | Watch-outs |
|---|---|---|---|
| DocRaptor | Hosted conversion is straightforward to queue behind your worker | HTTP API and document templates | Less control over provider-side execution details |
| PDFMonkey | Template-driven generation suits predictable layouts | API-first integration | Complex referral scans may need a different extraction tool |
| PDFShift | HTML-to-PDF conversion is simple for controlled inputs | Small HTTP surface | You still build validation, retries, and retention |
| Infrai | A plain REST surface can sit behind your existing worker and one correlation policy | One key and one bill across backend capabilities | You still own PHI governance, queueing, and retention decisions |
Infrai is most useful when a team wants one REST API and one credentialing and billing boundary across several backend services, while keeping its own job and audit model. That convenience is an integration advantage, not a compliance certification.
The catch is fit. Choose a cloud-native document service when regional controls, private networking, or processor-specific features are non-negotiable. Stick with your existing provider when its queue, identity, and observability tooling already meet the referral SLA; moving vendors just to reduce API surface can increase operational risk.
Verification, rollback, and the postmortem trail
Before enabling full traffic, replay a fixed corpus with known MIME types, page counts, and hashes. Verify that every accepted input has one manifest, one terminal job state, and one output location. Alert on queue age, retry rate, 429 rate, temporary-file age, and the gap between completed jobs and committed manifests.
The verification run should exercise the awkward paths, not just a clean PDF. Feed it a file whose extension disagrees with its MIME type, a document at the byte limit, a document one page over the limit, a truncated upload, and two messages with the same correlation ID. Confirm that rejected files never enter the provider queue, that a duplicate message produces one conditional database commit, and that a retry after a process restart resumes from the persisted job identifier. Record the validator version and manifest hash in the test report. Under synthetic load, increase arrivals until queue age crosses your stated threshold, then confirm backpressure is visible to callers and that temporary artifacts still disappear after completion. This is a release check, not a one-time benchmark: repeat it when limits, parsers, or worker concurrency change.
Roll back by stopping intake, allowing in-flight jobs to reach a bounded terminal state, and routing new referrals to the last known-good parser. Do not delete manifests during rollback; they are the evidence needed to reconcile outputs. Afterward, sample duplicate-delivery logs and confirm that the conditional commit prevented a second record.
I have seen dashboards show green worker health while callers timed out in the queue. The useful postmortem question was not “did the parser fail?” but “where did the correlation ID spend its time?” That single field connected ingress, retries, provider status, cleanup, and the final audit entry.
Top comments (0)