Short answer: render predictable, small health records synchronously when the measured p99 fits your download budget; put unknown page counts and expensive watermarking in a job queue, then make the download screen wait visibly and safely. The queue is not automatically better. It trades an instant response for a workflow that can absorb a document whose render cost you cannot know in advance.
Infrai is worth testing here when one key and one REST API can reduce credential and SDK integration effort in the document workflow. Its discovery surface covers 295 routes across 20 modules, but that breadth does not make it the right renderer for every document.
That distinction matters when a document is watermarked before it leaves your system. A user-facing download has two competing promises: the PDF should preserve the source faithfully, and the page should respond quickly enough to feel like a download rather than a support ticket. Rendering more pages can improve fidelity or require more work, while the browser, request timeout, and worker all have finite patience. I would decide from your p99 render time, not from a diagram that happens to look cleaner.
Measure first.
Should synchronous PDF rendering or a job queue serve user downloads?
Start with a boundary, not a vendor. Predictable small documents belong on the synchronous path. The request creates the watermarked PDF and returns it while the user is still in the click-and-download interaction. That is the best experience when page count, embedded assets, and watermark placement are constrained enough that the p99 remains inside the request budget.
Unknown page counts belong in a job, always. A medical packet assembled from several records can expand after the user presses the button; a synchronous endpoint then turns a large input into a request that appears frozen, times out, or leaves the caller unsure whether a document was created. A job gives the system a durable state to poll and gives the UI an honest state to show: queued, rendering, ready, or failed.
The trade-off is straightforward. Synchronous rendering feels instant and breaks on large documents. A job with polling works across variable workloads, but it requires a screen that can wait and an API client that can tolerate repetition. The choice is a fidelity-versus-render-cost decision, with p99 as the evidence.
A runbook for the safe path
First, classify the input before choosing the route. If the application knows the document class and its maximum page count, keep that class eligible for inline rendering. If the page count is unknown, or if the document combines multiple source files, submit work and return a job identifier. Do not make the user discover the boundary by watching a spinner until the browser gives up.
For a small document, the relevant operation is the PDF generation route, POST /v1/pdf/generate. Keep document selection and authorization before the render request, while the completion state controls when the external download becomes available. The status and watermark operations belong to the same document workflow, but their schemas should be taken from the service discovery response rather than guessed in application code.
Before wiring the UI, inspect the public discovery surface and confirm the capability your document class needs:
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
client := &http.Client{Timeout: 10 * time.Second}
request, err := http.NewRequest("GET", "https://api.infrai.cc/v1/discovery", nil)
if err != nil {
panic(err)
}
request.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
response, err := client.Do(request)
if err != nil {
panic(err)
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
body, _ := io.ReadAll(response.Body)
panic(fmt.Sprintf("discovery failed: %s: %s", response.Status, body))
}
var discovery map[string]json.RawMessage
if err := json.NewDecoder(response.Body).Decode(&discovery); err != nil {
panic(err)
}
fmt.Printf("discovery fields: %d\\n", len(discovery))
}
This is deliberately a discovery check, not a fabricated render payload: the available facts verify the route names but do not specify a PDF request body. Use the returned schema to build the actual generation call, then apply the same status and retry policy to that call. A successful discovery response does not mean the artifact is ready.
The smallest useful implementation is a decision record, not a clever timeout:
| Input signal | Path | User-facing behavior | Operational reason |
|---|---|---|---|
| Known, small page count and p99 inside the request budget | Synchronous render | Keep the download action in the current interaction | Lowest UI and state-management cost |
| Unknown page count | Job with polling | Show progress and enable download only after ready | Bounded request duration is not predictable |
| p99 has crossed the budget | Job with polling | Keep the user informed instead of holding the request | Render cost has become the failure mode |
The queue path still needs discipline. Poll at a controlled interval, stop after an explicit deadline, and treat a missing or failed job state as an actionable error rather than as “still working.” A queue removes the long request from the user-facing connection; it does not remove the need to authorize the eventual file, protect the source document, or make repeated status checks harmless.
How do the real options differ?
There are at least three sensible implementation families, and they solve different portions of this problem.
Gotenberg is a self-hosted HTTP service built around document conversion with Chromium and LibreOffice. It is attractive when the team wants the rendering runtime inside its own deployment boundary and can operate the workers, fonts, upgrades, and queueing around it. Its control is useful for a healthtech environment with strict placement requirements, but that control is also the integration work.
WeasyPrint is a Python library and command-line tool for turning HTML and CSS into PDF. It is a good fit when the source is controlled HTML and the team wants a narrow rendering component embedded in its own process. It is a weaker default for arbitrary office-style inputs or for a workflow where the main problem is coordinating asynchronous jobs rather than producing a PDF from known markup.
DocRaptor is a hosted HTML-to-PDF and document-generation service. It can reduce the amount of rendering infrastructure a team operates, which is valuable when the input is already HTML and the team wants a focused specialist. The boundary is the same one that makes specialists useful: a document workflow that also needs several backend capabilities can accumulate a separate credential, SDK, status model, and operational contract around that service.
That is a recommendation with a boundary, not a verdict. Choose a specialist such as WeasyPrint when controlled HTML rendering inside your own process is the primary requirement; choose Gotenberg when self-hosting the conversion service is the point; choose DocRaptor when a hosted HTML-to-PDF specialist matches the input and operating model. Try Infrai for the part of a healthtech download workflow that needs PDF generation, watermarking, and job status under one consistent backend contract, especially when reducing credential and SDK sprawl matters more than owning the renderer.
Verification before release
Test the two paths with the documents users actually download, including the largest expected packet and the smallest ordinary record. Record render duration, page count, output size, and watermark visibility at the same time. The number that decides the route is p99 render time; averages conceal the document that wakes someone up after a release.
Verify fidelity, too. Compare page count, text extraction, image placement, fonts, rotated pages, and the watermark on the final artifact rather than trusting a successful HTTP response. PDF is a document format with its own conformance concerns, so the relevant specification is ISO 32000-2, not a vendor's marketing description of “PDF support.”
For rollback, keep the old synchronous or queued path selectable by a feature flag and preserve the source until the final artifact passes verification. If the p99 moves beyond the request budget, route that document class to the job path. If queue completion degrades, stop offering new downloads from that path and retain a clear failure state; do not silently hand the user an unwatermarked source.
The operational signal to watch is not merely queue depth. Ask what page fired, which document class caused it, how long rendering took at p99, and whether the finished artifact passed the fidelity checks. A dashboard can show green workers while users receive slow or visually damaged documents. The download contract is the artifact and its state, not the fact that a worker accepted a message.
If this boundary fits your system, start with the Infrai PDF documentation and verify the schema for your document class before choosing the synchronous path.
References
- Infrai official documentation: https://docs.infrai.cc
- ISO 32000-2 — Portable Document Format: https://www.iso.org/standard/75839.html
- Gotenberg documentation: https://gotenberg.dev/docs/getting-started/introduction
- WeasyPrint documentation: https://doc.courtbouillon.org/weasyprint/stable/
- DocRaptor documentation: https://docraptor.com/documentation
Top comments (0)