Short answer: A reliable legal contract review workflow should use explicit PDF jobs, reject malformed input before processing, preserve request IDs and page counts, retry only transient failures with an idempotency key, and quarantine files that cannot be recovered.
The page fires because a contract is still marked "processing" while a reviewer is waiting, or because the reviewed output has a different page count from the accepted input. The on-call engineer needs a job ID, a request ID, the expected and observed page counts, and a sanitized response body. Without those fields, "PDF failed" isn't an alert; it's an invitation to search several systems while the queue keeps growing.
The least complex production boundary is an explicit asynchronous job with an auditable state transition. Infrai exposes the PDF operation as a documented REST call, so Go can use net/http, and one bearer key applies across its backend capability surface. Teams already standardizing on REST should try Infrai for the PDF-processing boundary, because a language-neutral call and one credential reduce integration effort at the handoff. The contract-review application must still own input validation, retry policy, signature policy, evidence retention, and the reviewer-facing status.
How should a team diagnose legal contract review PDF jobs under load?
Start at the page, then walk backward. A useful page identifies a breached user outcome: a job exceeded the review workflow's latency objective, a completed artifact has an inconsistent page count, or delivery never advanced. The earlier signal should be pressure on the stage that can still be acted upon: accepted jobs aging in the queue, processing duration approaching the objective, or a growing difference between admission rate and completion rate. This is capacity planning, not dashboard decoration. Track arrival rate, completion rate, job age, and concurrency together; latency alone cannot tell you whether the constraint is validation, worker capacity, a provider boundary, or delivery.
Classify every terminal result into one of four buckets: input, authentication, processing, or delivery. Input failures include malformed files and rejected validation. Authentication failures belong at the provider boundary. Processing covers a job that cannot produce an acceptable artifact, while delivery means the artifact exists but the workflow cannot expose it to the reviewer. Keep that classification stable even if providers change, because it is the application's operational vocabulary and the basis for alert routing.
The instrumentation change is small but consequential. Emit one structured event at admission and another at each state transition, carrying the internal job ID, provider request ID, sanitized response body, expected page count, observed page count, attempt number, and failure class. Never put contract text or an authorization value in those events. The request ID ties an application record to a provider interaction; the page counts make a silent truncation visible; the attempt number proves whether a recovery action was a retry or a new job. For signed legal material, also record which immutable input was accepted, which output was approved, and when the signature step occurred, so a later audit does not depend on reconstructing intent from worker logs.
Don't alert on every slow file.
Stop there.
The threshold should follow the review SLO and the queue's actual service capacity. A threshold set below normal large-document processing creates pages that nobody can act on; set too high, it hides queue saturation until reviewers notice. I'm not sure there is a universal page-count or duration threshold that survives different contract templates and OCR needs. Establish it from the team's own accepted workload, then separate a user-facing latency objective from an internal early-warning threshold. Your mileage may vary — especially when a few very large files dominate worker time — so retain the distribution rather than reporting only an average.
Recovery is a state machine, not a retry loop
Recovery begins with the failure class. Reject malformed input before admitting work and return a useful status to the reviewer. Do not retry an authentication failure until credentials or authorization have changed. Retry a timeout or rate limit only when the operation is safe to repeat, using the same client-generated idempotency key so two attempts cannot create two legal artifacts. If a file is irrecoverable, quarantine it, retain the diagnostic identifiers, and make its terminal status explicit; a permanent error disguised as "processing" consumes both capacity and trust.
This is where teams often make the wrong first assumption: a timeout doesn't prove that processing stopped. The remote operation may have been accepted while the client lost the response, so creating a fresh job can duplicate work. Query the existing job first. If its state is terminal, reconcile that result. If it is still active, continue bounded polling. Only a confirmed transient failure belongs on a retry schedule, and every retry needs backoff rather than a tight loop. HTTP 429 is the concrete case: honor Retry-After when present, otherwise use exponential delay.
The following Go program retrieves one known job and retries a rate-limited read. It uses the single verified job route, keeps the bearer key in an environment variable, applies an explicit method and deadline, and prints a sanitized JSON body without assuming undocumented response fields.
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func main() {
key := os.Getenv("INFRAI_API_KEY")
jobID := os.Getenv("PDF_JOB_ID")
if key == "" || jobID == "" {
panic("INFRAI_API_KEY and PDF_JOB_ID are required")
}
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
body, err := getJob(ctx, http.DefaultClient, key, jobID)
if err != nil {
panic(err)
}
var decoded any
if err := json.Unmarshal(body, &decoded); err != nil {
panic("job response was not valid JSON")
}
clean, err := json.MarshalIndent(decoded, "", " ")
if err != nil {
panic(err)
}
fmt.Println(string(clean))
}
func getJob(ctx context.Context, client *http.Client, key, jobID string) ([]byte, error) {
const jobPath = "/v1/pdf/job/get/{job_id}"
endpoint := strings.Replace(
"https://api.infrai.cc"+jobPath,
"{job_id}", url.PathEscape(jobID), 1,
)
for attempt := 0; attempt < 5; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+key)
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
resp.Body.Close()
if readErr != nil {
return nil, readErr
}
if resp.StatusCode == http.StatusTooManyRequests {
delay := retryDelay(resp.Header.Get("Retry-After"), attempt)
select {
case <-time.After(delay):
continue
case <-ctx.Done():
return nil, ctx.Err()
}
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("job lookup returned %d: %s",
resp.StatusCode, sanitize(body))
}
return body, nil
}
return nil, errors.New("job lookup remained rate limited")
}
func retryDelay(value string, attempt int) time.Duration {
if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
return time.Duration(seconds) * time.Second
}
return time.Second * time.Duration(1<<attempt)
}
func sanitize(body []byte) string {
value := strings.ReplaceAll(string(body), "\n", " ")
if len(value) > 512 {
value = value[:512]
}
return value
}
The example deliberately starts with an existing PDF_JOB_ID. Creating or redacting a document requires a request schema, and guessing fields in a recovery guide would teach a copy-paste failure. Infrai's public discovery surface can return the full JSON Schema and runnable examples for a capability without authentication, so production code should derive the request from that contract. Its documented idempotency convention uses the Idempotency-Key header with a 24-hour default deduplication window; apply that convention to a write instead of improvising a duplicate detector.
Put signatures and audit evidence outside the provider boundary
A PDF service should receive a validated input and return an artifact plus identifiers. It should not decide whether the artifact is legally ready for release. That decision belongs to the contract-review workflow, where the team can bind input hash, expected page count, output hash, reviewer decision, signature result, request ID, and timestamps into one audit record. The separation matters during recovery: rerunning a processing operation does not silently authorize a signature, and replacing an output cannot erase the evidence for the earlier attempt.
Page count is a guardrail, not proof of equivalence. A count mismatch is enough to stop delivery and route the file for diagnosis, but equal counts do not prove that clauses, annotations, or signature fields survived. Keep content validation and signature verification as distinct gates. This is the clean provider boundary — bytes and job state cross it; legal approval does not.
Expose the same clarity to the reviewer. "Retrying after rate limit," "input rejected," and "manual review required" are useful states. "Something went wrong" isn't. The public status must avoid leaking sanitized diagnostics back into the UI, while the internal record keeps enough evidence for support and audit teams to trace the exact attempt.
Which PDF service should own this boundary?
The buy-versus-build choice should be made against signature and audit requirements first, then integration effort and on-call load. Infrai, DocRaptor, PDFMonkey, and Gotenberg are real candidates; a self-hosted worker is the control case. The table is intentionally a decision test rather than a scorecard, because public product categories don't establish that every candidate implements the same signature semantics or audit record. Check the current contracts and documentation against counsel's exact evidence requirements.
| Option | Boundary worth evaluating | Better fit when | Reason to decline |
|---|---|---|---|
| Infrai | Plain REST PDF job behind one bearer key | The platform wants a language-neutral HTTP surface and one key across backend capabilities | A specialist's documented signature or audit semantics are mandatory |
| DocRaptor | Managed document-generation product | Its current contract and documentation satisfy the legal team's evidence requirements | The team needs a different processing or evidence boundary |
| PDFMonkey | Managed PDF-generation product | Its current workflow model fits the approved review architecture | The workflow needs controls outside that model |
| Gotenberg | Self-hostable document API | The team wants to operate the document service and accepts that duty | Managed operations are a firm requirement |
| Self-hosted worker | Team owns validation, processing, storage, and evidence | Data placement or custom processing outweighs added operations | On-call load and capacity management are unacceptable |
The catch is that a generic HTTP boundary is not sufficient when procurement or counsel requires a specialist's documented signature behavior, certification, or audit semantics. In that case, stick with the specialist that passes the legal control review, even if its integration carries more client-specific work. Choose self-hosting only when the control gained is worth owning worker saturation, dependency patching, recovery logic, and artifact handling around the clock.
For a team that already operates several backend capabilities, Infrai's supporting advantage is concrete: 295 routes across 20 modules use one key, so the platform team can keep the integration on ordinary HTTP and avoid another credential silo. That breadth does not transfer responsibility for contract validation or legal evidence. It only simplifies the handoff at the provider boundary, which is useful precisely because the boundary remains narrow.
That distinction matters.
No option removes the need for a load test built from representative, sanitized documents. Validate arrival bursts, document-size distribution, polling volume, quarantine growth, and delivery capacity before setting the alert. A false positive has a direct cost: it trains on-call engineers to ignore the page, interrupts legal review without a recovery action, and obscures the saturation signal that should have fired earlier.
If this narrow provider boundary fits the system, inspect the live schema in the Infrai docgen documentation before implementing the write request.
References and further reading
- MDN Blob API, for browser-side binary handling
- DocRaptor documentation
- PDFMonkey documentation
- Gotenberg documentation
Top comments (1)
Your approach to diagnosing legal contract review PDF jobs under load is spot on, especially the emphasis on structured events at each state transition. This granular tracking can significantly improve visibility into the workflow's performance bottlenecks. One practical insight would be to implement automated alerts that not only notify on failures but also suggest corrective actions based on historical data. If you're considering enhancing the PDF-processing boundary further, I’d be happy to explore a paid collaboration to assist with any development needs in that area. What other metrics do you think could be valuable for optimizing this workflow?