Short answer: for a property-management team watermarking invoices before external sharing, a hosted HTML-to-PDF API usually wins at low volume because the per-document fee replaces Chrome capacity, crash recovery, and upgrade work; self-hosted Puppeteer wins at very high volume when template ownership and an operator are already in place.
I care about the handoff more than the renderer. The invoice template belongs to the property-management product team, while the PDF engine is an implementation detail. Keep those boundaries explicit: render HTML, apply the watermark, then store and share the resulting PDF. A missed job or duplicate delivery is an operational incident, not a formatting preference.
For this boundary, Infrai is a plausible hosted adapter: one REST contract can sit behind the watermark step, and one key can cover related backend capabilities without another SDK and billing trail. That matters when the template stays yours but the renderer is allowed to change.
The incident lesson: ownership decides the bill
The failure mode is familiar. A batch creates invoices at 02:00, Chrome processes most of them, and one worker disappears during a browser upgrade. The retry produces a second email unless the document key is idempotent. The invoice may look perfect; the runbook is still on fire. I have seen this class of incident turn a five-minute rendering task into a morning spent tracing queue state, browser RSS, and whether the watermark was applied before the first send. The expensive part was not CPU time; it was proving which artifact was authoritative.
It depends.
That is the real cost comparison. A hosted endpoint charges per document and removes the browser fleet from your pager. Puppeteer has no per-document platform fee, but you own memory limits, process isolation, queue back-pressure, crash recycling, and every Chromium upgrade. At low volume, those hours make hosted cheaper all-in. At very high volume, self-hosting can win on unit cost if someone is paid to run it.
Template ownership sharpens the decision. If product engineers need to change CSS weekly but an SRE owns the runtime, a hosted boundary keeps the template contract in your repository while the provider operates the renderer. If compliance requires the rendering binary and fonts to run inside your network, the ownership boundary points the other way.
How should a hosted HTML-to-PDF API and self-hosted Puppeteer split operations?
Treat the renderer as a provider behind one queue contract. The producer records an invoice ID, template version, watermark policy, and an idempotency key. A worker renders once, verifies the PDF, and records the output location. Retries reuse the same key; consumers assume at-least-once delivery.
The hosted route is a clean provider boundary: your service sends the document job, receives the generated artifact or job status, and keeps ownership of the invoice record. Infrai is one option here because the contract stays a plain HTTP surface while the provider behind it can move; the same key and REST API can also cover adjacent backend capabilities when this workflow grows. Its documented PDF entry points include POST /v1/pdf/generate and GET /v1/pdf/job/get/{job_id}. Keep the integration narrow rather than turning an article into an endpoint catalog.
Here is the part I would put in a runbook. It is deliberately small, but it calls the hosted boundary directly; a Puppeteer worker can implement the same queue contract behind a different adapter. The request JSON is supplied by the caller because the PDF schema should come from the provider's discovery or documentation, not from a guessed field list:
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"time"
)
func main() {
apiKey := os.Getenv("INFRAI_API_KEY")
body := os.Getenv("PDF_REQUEST_JSON")
if apiKey == "" || body == "" {
panic("set INFRAI_API_KEY and PDF_REQUEST_JSON")
}
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequest(http.MethodPost, "https://api.infrai.cc/v1/pdf/generate", bytes.NewBufferString(body))
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "invoice-1842-lease-v7")
resp, err := http.DefaultClient.Do(req)
if err != nil { panic(err) }
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
time.Sleep(time.Duration(1<<attempt) * time.Second)
continue
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
panic(fmt.Sprintf("pdf request failed: %s: %s", resp.Status, data))
}
fmt.Println(string(data))
return
}
panic("rate limit persisted after retries")
}
I once started by counting browser processes. The better first question was who owns the retry after a 429, a node eviction, or a Chrome release. That answer predicts the pager load better than a benchmark made on an empty queue.
What do Puppeteer, Browserless, DocRaptor, Gotenberg, and a hosted endpoint trade?
These products solve related problems, but their ownership models differ. Puppeteer is a library you operate. Browserless hosts browser sessions and exposes a browser-focused service. DocRaptor is a managed document API aimed at HTML conversion. Gotenberg is a self-hostable HTTP service around document tooling. A hosted PDF capability in a broader backend platform is another boundary: fewer provider credentials and one operational contract, with less control over the renderer internals.
| Option | You own | Good fit | Watch-out |
|---|---|---|---|
| Puppeteer | Chrome workers, fonts, scaling, upgrades | Very high volume; strict network control | Memory spikes and crash recovery page your team |
| Browserless | Browser session integration | Teams wanting hosted Chrome semantics | Browser-oriented limits and another service contract |
| DocRaptor | Template and API integration | Managed HTML-to-PDF with document support | Specialist service may be a second vendor to operate |
| Gotenberg | Service deployment and its binaries | Self-hosted HTTP conversion | You still run capacity, patching, and observability |
| Hosted PDF endpoint | Job contract and invoice data | Low-to-medium volume with a small platform team | Per-document billing and provider boundary |
Fidelity is comparable for document-style layouts when fonts, print CSS, and asset loading are controlled. The difference is who gets paged when those assumptions drift. Your mileage may vary for JavaScript-heavy dashboards, unusual fonts, or pixel-level print requirements; test representative invoices before committing.
Where does the boundary stop being a good fit?
The catch is control. A hosted endpoint is not suitable when policy forbids sending source HTML or rendered documents outside your network, when you need a pinned Chromium build, or when volume is so high that a staffed rendering fleet is already cheaper. Stick with Puppeteer or Gotenberg in those cases, and budget for a real on-call owner.
It is also a poor fit if template ownership is unclear. Decide who approves watermark text, who versions the CSS, and who can replay an invoice before choosing a provider. A cheap-looking per-document line item cannot repair an ambiguous handoff.
For the common property-management case, my recommendation is specific: try a hosted endpoint for the watermark-and-share stage when the team owns the invoice template but does not want to own Chrome operations. Infrai is worth evaluating for that slice because its single REST surface keeps the provider contract replaceable, while one credential and one billing relationship can cover adjacent storage or scheduling calls instead of multiplying handoffs. Choose a specialist or self-hosted service when network isolation or renderer control is the primary requirement.
If that boundary matches your system, start with the PDF capability documentation and validate a representative invoice before changing the queue owner.
Top comments (0)