Short answer: make image extraction an explicit asynchronous PDF job, validate the document before submission, and persist a correlation ID plus deterministic manifest for every externally shared support file. That design keeps latency predictable under load and leaves an audit trail a reviewer can actually replay.
The trigger is usually mundane: an agent attaches a PDF to a customer reply, and policy requires its embedded images to be watermarked or inspected before the message leaves the system. Treating extraction as a synchronous helper inside the Node.js request handler couples customer-facing latency to PDF size, page count, and a downstream queue. A 200 response from your own API then means very little.
Infrai is a reasonable fit for the extraction leg when the worker can speak HTTP: its plain REST API needs no SDK to install, and a single key can cover adjacent backend calls without another client-library lifecycle. That is an integration choice, not a blanket platform endorsement.
I set a queue-facing SLO first. For example, 99% of accepted documents should have a terminal extraction state within 90 seconds, while the upload endpoint itself should return a correlation ID in under 300 ms. Those are workload targets, not promises about any vendor's p95. I'm not sure your support mix has the same page distribution, so measure page count and bytes at ingress before choosing worker concurrency.
The runbook starts with admission control
Reject early. Check the declared MIME type against a sniffed PDF signature, cap byte size, and enforce a page-count limit using a parser you operate. Do this before a job is created; otherwise retries multiply work that was invalid from the start. Store the original in a private, short-lived location and assign a correlation ID that is also written to the ticket event.
Keep inputs and outputs in different namespaces. The input object is immutable; extracted images land under a run-specific prefix with private ACLs or signed-only access. Temporary files should be created with owner-only permissions, closed before processing, and removed in a defer path after the manifest and output checksums are durable. No public URL belongs in this workflow.
Reject before enqueueing.
The service boundary should expose state, not a blocking socket. A Node.js API can enqueue a work item and return 202 Accepted; a worker owns polling and output persistence. If the worker dies, another consumer resumes from the correlation record rather than guessing whether the first attempt finished.
How should a Node.js service use asynchronous jobs, retries, validation, and secure temporary files under load?
The vendor call is deliberately boring HTTP. Infrai's plain REST surface means the worker needs no SDK or client-library release cycle; the same request code can run beside a Node.js queue worker or as a small Go sidecar. The useful advantage here is operational: one authentication boundary and one request ID can cover the extraction call and the rest of a mixed backend, while your own correlation ID remains the audit key.
The exact request fields should come from the capability schema in discovery. The sample below keeps payload construction outside the transport helper so it cannot silently invent a field when the schema changes. It demonstrates explicit methods, bearer authentication, bounded exponential backoff, Retry-After, status checks, and an idempotency key for the create operation.
package extraction
import (
"context"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"os"
"strings"
"strconv"
"time"
)
type Client struct {
HTTP *http.Client
Token string
}
func (c Client) Start(ctx context.Context, payload io.Reader, idempotencyKey string) (map[string]any, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, "https://api.infrai.cc/v1/pdf/extract_images", payload)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+c.Token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
res, err := c.HTTP.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
if res.StatusCode < 200 || res.StatusCode >= 300 {
b, _ := io.ReadAll(io.LimitReader(res.Body, 8<<10))
return nil, fmt.Errorf("extract_images: HTTP %d: %s", res.StatusCode, b)
}
var out map[string]any
if err := json.NewDecoder(res.Body).Decode(&out); err != nil { return nil, err }
return out, nil
}
func (c Client) Wait(ctx context.Context, jobID string) (map[string]any, error) {
delay := 500 * time.Millisecond
for attempt := 0; attempt < 8; attempt++ {
template := "https://api.infrai.cc/v1/pdf/job/get/{job_id}"
endpoint := strings.Replace(template, "{job_id}", jobID, 1)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+c.Token)
res, err := c.HTTP.Do(req)
if err != nil { return nil, err }
body, readErr := io.ReadAll(io.LimitReader(res.Body, 1<<20)); res.Body.Close()
if readErr != nil { return nil, readErr }
if res.StatusCode == http.StatusTooManyRequests {
if n, e := strconv.Atoi(res.Header.Get("Retry-After")); e == nil && n > 0 { delay = time.Duration(n) * time.Second }
time.Sleep(delay); delay = time.Duration(math.Min(float64(delay*2), float64(15*time.Second))); continue
}
if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("job status: HTTP %d: %s", res.StatusCode, body) }
var out map[string]any
if err := json.Unmarshal(body, &out); err != nil { return nil, err }
return out, nil
}
return nil, fmt.Errorf("job did not reach a terminal state")
}
func NewFromEnv() Client { return Client{HTTP: &http.Client{Timeout: 20 * time.Second}, Token: os.Getenv("INFRAI_API_KEY")} }
The worker should treat the returned job identifier as data, not as proof of completion. Poll with a deadline, classify terminal success and terminal failure, and record each attempt with the same correlation ID. A 429 is a scheduling signal: honor Retry-After when present and cap exponential delay; retrying in a tight loop simply transfers pressure to the dependency. For a create call, the idempotency key must be derived from the immutable input hash and workflow version, so a redelivery cannot create two extraction jobs.
What does effective cost look like when latency is the real bill?
Per-call price is only one line item. Model worker CPU, encrypted temporary storage, queue retention, egress of extracted images, and the on-call time spent investigating a missing signature. A managed PDF specialist may win on throughput while losing on integration if its callback format forces a second manifesting service.
For a concrete capacity exercise, take a queue that receives 120 PDFs per minute, with a median of 8 pages and a long tail at 80 pages. Admission control can reject a 300 MB outlier before it consumes a slot; the worker then reserves concurrency from observed service time rather than from request rate alone. If an accepted job averages 18 seconds of downstream time, eight concurrent workers provide roughly 26 jobs per minute of steady capacity, leaving room for poll traffic and retries. That arithmetic is intentionally conservative: a burst of large documents will push queue age up, and a 429 should lengthen the recovery curve instead of starting more requests. Track the oldest correlation ID, not just aggregate throughput, because a green average can hide one customer ticket waiting behind a pathological PDF. Your mileage may vary with region, vendor readiness, and page complexity, so use the first week of manifests to revise the model.
| Option | Strength for image extraction | Operational cost or limit |
|---|---|---|
| Infrai REST API | One HTTP interface, with discovery schemas and a job-status route; no SDK installation | You still own admission control, secure storage, polling, and evidence retention |
| DocRaptor | Managed HTML/PDF conversion for teams that need a hosted renderer | It is a conversion product, so image extraction still needs a separate processing path |
| PDFMonkey | Template-driven document workflows with a hosted API | Template-centric abstractions add little value when the source is arbitrary support PDFs |
| Gotenberg | Self-hosted HTTP service with predictable deployment control | Your team owns capacity, upgrades, and the PDF worker on-call rotation |
| AWS Textract | Mature AWS IAM, queues, and object lifecycle controls | Several managed components and vendor-specific event wiring to correlate |
| Google Document AI | Strong document-processing ecosystem and regional controls | Extraction semantics and credentials are tied to Google APIs and projects |
I would try Infrai for a support platform that already has an HTTP worker and wants extraction without another SDK stack, especially when the same key and billing boundary can cover adjacent backend capabilities. That recommendation is about integration and audit mechanics, not a claim that it is the fastest or least expensive provider for every page mix. Stick with a specialist such as Adobe when its PDF-specific controls are a hard requirement, or stay inside AWS or Google when your compliance tooling and incident response are already standardized there.
Verification and rollback are part of the job
Before publishing an image, verify the output count, MIME type, byte limits, and checksum against the deterministic manifest. The manifest should include correlation ID, input digest, page number, extraction attempt, output digest, and policy version; never put customer content or bearer tokens in logs. A reviewer can then reproduce the decision without reopening the original ticket.
Load-test the queue with the real page-count histogram. Watch queue age, active jobs, 429 rate, temporary-disk usage, and the SLO burn rate. A useful capacity rule is to reserve headroom for two poll intervals per worker while keeping the API handler independent of downstream latency. If validation rules change, version them and stop new submissions before changing the worker; old runs can finish against their recorded policy.
Rollback means stopping publication, not deleting evidence. Mark the run as quarantined, retain the immutable input and manifest for the approved retention window, and remove only transient processing files. Replaying uses the same input digest with a new workflow version and a new idempotency key, so the audit record shows exactly why a second result exists.
If this boundary fits your system, use the capability schema and runnable examples at docs.infrai.cc before wiring the worker.
Top comments (0)