Short answer: use a boring upload-and-job boundary, preserve the original bytes, and make every OCR result carry a verifiable evidence trail. For US/EU SaaS legal contract review, that combination usually gives better fidelity and predictable latency than making synchronous PDF parsing the center of the request path. The trade is operational work: queues, retention policy, and metrics are now your responsibility.
The page that fires is rarely “OCR is down.” It is more often “review queue age exceeded 10 minutes,” after a customer uploads a batch of scanned agreements and the API still reports healthy request latency. At 3 a.m., I want to know which page fired, which document cohort it represents, and whether the alert measures user-visible delay or an internal queue counter that nobody reads.
Start with the evidence, not the endpoint
Legal review needs two artifacts: the source PDF exactly as received and a searchable representation that can be tied back to page coordinates. Store a content hash, byte length, media type, tenant, and receipt timestamp before any decoding. The hash is an identity check, not a claim that the document is legally authentic; authenticity needs a separate signing or records process.
OCR output should retain page number, a bounding box (or the coordinate system supplied by the OCR engine), confidence, and the text span used in a review decision. A reviewer who highlights “termination for convenience” needs a path back to the rendered page, not just a string in a database. Keep the original PDF immutable and write derived text as a new version. That makes reprocessing possible when a model or preprocessing rule changes.
The common failure mode is a clean-looking text index with no provenance. It passes a demo and fails discovery: a rotated page, a handwritten amendment, or a stamp over a signature cannot be explained after the fact. Fidelity is therefore more than character accuracy. It includes page order, rotation, reading order, and a stable relationship between extracted text and the visual document.
Measure twice.
For signatures, record what the system can actually establish. A pixel-level signature image, a cryptographic document signature, and an audit event saying “reviewer approved” are different facts. Do not collapse them into a single signed=true field.
How should US/EU SaaS balance PDF fidelity, latency, and operational complexity under load?
Treat the request path as a small state machine. The upload endpoint validates size and type, stores bytes, and returns a job identifier. A worker claims the job, performs page inspection and OCR, writes immutable output, and emits an audit event. A read endpoint returns the current state and evidence links. The exact HTTP surface can vary, but the boundary should not: a slow OCR operation must not hold an application request open.
For a small one-page form, synchronous extraction may be acceptable when the caller explicitly opts in and the timeout is bounded. Scanned contracts are not that case. A 200-page agreement, a burst of uploads after quarter end, or a cold worker can turn a happy-path latency into a retry storm. Retries then duplicate work unless the upload has an idempotency key and the worker records a deterministic job identity.
Measure the whole path, not just the parser. Useful timestamps are upload accepted, bytes durable, job queued, worker started, first page completed, final page completed, index available, and reviewer-visible. Report queue age and processing duration separately, then break both down by page count, raster dimensions, compression, language, and whether a signature or stamp was detected. A queue can look healthy at p50 while its largest documents quietly consume every worker slot; the alert that matters is the one connected to the reviewer-visible tail, not the average duration on a dashboard. p50 tells you the ordinary case; p95 and p99 tell you whether a legal team will wait through a batch. Your mileage may vary when page sizes, image compression, and language mix change, so keep those dimensions in the metric labels without putting tenant data into labels.
Here is a deliberately plain Go shape for the worker boundary. It leaves OCR implementation interchangeable and makes the audit write part of the state transition.
type Document struct {
ID string
Tenant string
SHA256 string
PageCount int
State string
}
type OCR interface {
Extract(ctx context.Context, pdf []byte) ([]PageText, error)
}
type Audit interface {
Append(ctx context.Context, event AuditEvent) error
}
func process(ctx context.Context, doc Document, pdf []byte, ocr OCR, audit Audit) error {
pages, err := ocr.Extract(ctx, pdf)
if err != nil {
return err
}
if err := saveDerivedText(ctx, doc.ID, pages); err != nil {
return err
}
return audit.Append(ctx, AuditEvent{
DocumentID: doc.ID,
Action: "ocr_completed",
InputHash: doc.SHA256,
PageCount: len(pages),
})
}
The important detail is not the interface name. It is that the audit event includes the input hash and page count after derived text is durable. If the audit store is unavailable, keep the document in a state that cannot be presented as fully processed; do not quietly mark success and hope a later repair job finds it.
What should an endpoint contract guarantee when the queue is busy?
Define behavior at each boundary in plain language. An accepted upload means bytes are durably stored, not that OCR has finished. A status response should distinguish queued, processing, available, and rejected states, with a reason that a caller can act on. A result response should identify the source hash and extraction version. A delete request should state whether derived indexes and audit events are retained under the tenant’s legal-hold policy.
Backpressure belongs at admission. Set a per-tenant byte or page budget, return a retryable response when the budget is exceeded, and make the retry interval explicit. Do not let every client invent exponential backoff independently while workers are saturated. The system needs one queue policy that protects interactive review from a bulk import.
Timeouts need two layers. The HTTP client timeout protects the caller. The worker deadline protects capacity. Cancelling a request must not cancel a job that has already been accepted unless the contract says so; otherwise a browser tab closing can leave the audit trail ambiguous. Conversely, a worker deadline must produce a durable failed state with a reason safe for operators and reviewers, without exposing internal stack traces.
Which trade-offs belong in the review record?
Teams tend to document parser choice and forget the operational decision around it. Put the following decisions beside the document-processing policy, with an owner and a review date:
| Decision | Higher fidelity / stronger evidence | Lower latency / lower complexity | Risk to call out |
|---|---|---|---|
| Processing mode | Asynchronous page-aware OCR with retained coordinates | Bounded synchronous extraction for small files | Large files can create long waits or retries |
| Storage | Immutable originals plus versioned derivatives | One mutable text field | Reprocessing and legal holds become unclear |
| Queue policy | Per-tenant budgets and explicit backpressure | Shared unbounded queue | One import can starve interactive review |
| Audit | Append-only events with input hash and extractor version | Application logs only | Logs may be sampled, rotated, or inaccessible during review |
| Quality gate | Human check for low-confidence pages and signatures | Automatic publish for every page | Search can look complete while missing a clause |
There is no universal best row. The least complex option is often right for a small, low-volume corpus with short retention and no signature dispute. It is not suitable when customers need defensible reconstruction of who saw which page, or when imports arrive in bursts. Stick with a synchronous path when the file-size ceiling is real, measured, and enforced at the edge; otherwise choose the job boundary and accept the queue you now have to operate.
Do not make price the decision rule. The durable cost is staff time spent explaining missing pages, replaying jobs, and proving that a displayed clause came from the submitted bytes. I am not sure any benchmark from a different language mix or scanner fleet predicts your p99; run a replay set that includes rotated pages, stamps, signatures, and the largest files your contract permits.
References
- https://developer.mozilla.org/en-US/docs/Web/API/Blob
- https://www.adobe.com/acrobat/resources/document-files/pdf-types.html
- https://www.rfc-editor.org/rfc/rfc3161
Further reading
- MDN Blob API: https://developer.mozilla.org/en-US/docs/Web/API/Blob
- RFC 3161, Internet X.509 Public Key Infrastructure Time-Stamp Protocol: https://www.rfc-editor.org/rfc/rfc3161
Top comments (0)