PDF generation cost per document against self-hosting Gotenberg
The per-call rate is the easy number. The comparison that matters includes the container, the memory ceiling and the on-call pager.
The reason to generate documents on Infrai isn’t the per-call rate — it’s that the render, the storage it lands in, the email that delivers it and the queue that schedules it are one credential and one bill. A document pipeline is never one call, and the account count is what usually costs you.
Still, “is this cheaper than running Gotenberg” is a fair question with a real answer, and the answer isn’t only about rates.
The rate, read live
curl -sS "https://api.infrai.cc/v1/discovery/pdf.generate" \
-H "Authorization: Bearer ${INFRAI_API_KEY}"
{
"id": "pdf.generate",
"method": "POST",
"path": "/v1/pdf/generate",
"minimum_tier": "standard",
"billing": {
"is_billable": true,
"unit": "per_call",
"price_usd": 0.015,
"currency": "USD",
"approximate": false
}
}
Per call, approximate: false, verified 2026-09-21. The other operations have their own rates — OCR, parse, compress, convert and redact each read from GET /v1/discovery/{capability} — and all of them drift downward as vendor contracts improve, so read them rather than trusting a table.
GET /v1/account/usage shows what you actually spent, broken down per capability, which is the only figure worth planning with.
What self-hosting actually costs
A Gotenberg container is free to download.
The cost is everything around it, and the list is longer than anyone estimates at the start.
Chromium’s memory footprint under concurrency is the first surprise: a renderer handling four simultaneous documents wants more RAM than the instance someone sized for a web app, and the failure mode is an out-of-memory kill mid-render rather than a graceful queue. So you size for peak, which means paying for idle capacity between peaks.
Then the font packages, because a document that renders correctly on a developer’s Mac and wrongly in a slim Linux image is a font problem, and fixing it means a bigger base image and a list of packages somebody maintains. Then the queue in front of it, because concurrency has to be bounded somewhere. Then the upgrade when a Chromium security advisory lands.
| Cost | Self-hosted | Per-call API |
|---|---|---|
| Per document | near zero | a per-call rate |
| Instance sized for peak | continuous | none |
| Memory tuning and OOM handling | yours | none |
| Font packages and base-image size | yours | none |
| Chromium security updates | yours | none |
| Bounding concurrency | you build a queue | rate limits, plus your own queue if you want |
| Latency under load | predictable, you control it | depends on the platform |
Row seven is the honest advantage of self-hosting, and it’s a real one. If you render thousands of documents an hour and need predictable p99 latency, running your own gives you control that an API can’t, and the arithmetic favours you.
Work out your own crossover
import os
import requests
API = "https://api.infrai.cc"
KEY = os.environ["INFRAI_API_KEY"]
SESSION = requests.Session()
SESSION.headers.update({"Authorization": f"Bearer {KEY}"})
def rate(capability: str) -> float:
resp = SESSION.get(f"{API}/v1/discovery/{capability}", timeout=20)
resp.raise_for_status()
return float((resp.json().get("billing") or {}).get("price_usd") or 0.0)
def pdf_spend() -> dict:
"""What PDF work actually cost this period, from the same breakdown that prices
everything else. No vendor export, no estimating."""
resp = SESSION.get(f"{API}/v1/account/usage", timeout=25)
resp.raise_for_status()
data = resp.json()["data"]
rows = {r["key"]: r for r in data.get("breakdown", []) if r["key"].startswith("pdf.")}
return {"period": data.get("period"),
"pdf_total": round(sum(r["cost"] for r in rows.values()), 4),
"by_operation": {k: {"cost": round(v["cost"], 4), "calls": v["calls"]}
for k, v in rows.items()},
"account_total": data.get("total_cost")}
def crossover(monthly_infra_usd: float, ops_hours_per_month: float,
hourly_rate_usd: float = 75.0) -> dict:
"""Where does a per-call rate stop being cheaper? Include the hours, because a
renderer nobody maintains is not the option anyone is actually comparing."""
per_call = rate("pdf.generate")
fully_loaded = monthly_infra_usd + ops_hours_per_month * hourly_rate_usd
return {"per_call_usd": per_call,
"self_hosted_monthly_usd": round(fully_loaded, 2),
"breakeven_documents_per_month": int(fully_loaded / per_call) if per_call else None}
if __name__ == "__main__":
print(pdf_spend())
print(crossover(monthly_infra_usd=60.0, ops_hours_per_month=2.0))
That ops_hours_per_month argument is the one people leave out, and leaving it out is why self-hosting looks free. Two hours a month of somebody’s attention — a memory tune, a base-image bump, one incident — is usually a larger number than the infrastructure line.
What the structural facts are
Three things survive any repricing. There’s no plan minimum and no monthly platform fee, so a product generating fifty documents a month pays for fifty documents. Chained operations take a pdf_id rather than bytes, so a generate-compress-archive pipeline is three calls without three uploads. And the $2 every new account starts with covers enough renders to test your real templates before committing.
When to run your own anyway
High and steady volume with latency requirements. If documents are your core loop rather than a side effect — a print service, a bulk mail house, a reporting platform rendering thousands an hour — the per-call rate adds up and you have the volume to justify the operational work. Self-hosted Gotenberg or a direct Puppeteer setup is the right answer there, and it also gives you full browser features including JavaScript execution.
The honest limitation of the hosted path: you don’t control the renderer, so you can’t install a font, run JavaScript in the page, or tune the timeout. PDFShift and PDFMonkey sit in the same hosted bracket with richer template editors, and going direct to one of them is worth pricing if document generation is most of what you need.
Where the gateway wins is the pipeline. The render, the PUT /v1/storage/object/put/{bucket}/{key} that archives it, the POST /v1/email/send that delivers it and the POST /v1/cron/create that schedules the batch are one key and one invoice — so the comparison isn’t “per call versus a container”, it’s “one integration versus four”.