ReportLab PDF Generation: API vs Library Guide
ReportLab PDF generation explained with code, failure modes, API trade-offs, costs, and production patterns for developers shipping PDFs.
TL;DR
- ReportLab PDF generation is still a strong local option for small workloads, but production systems often move to APIs when Chromium, fonts, and worker management become operational concerns.
- PDFGeny returns rendered PDFs through
POST https://pdfgeny.com/api/v1/render, using headless Chromium by default, WeasyPrint as a second engine, and Ghostscript for PDF/A-2b output.
- PDFGeny median render time is 0.6 seconds, with a free plan of 50 documents per month without a card and overage pricing of $0.009 per document.
- Batch PDF generation through PDFGeny supports up to 100 documents in one call, with sync jobs, async jobs, HMAC-signed webhooks, and stored documents.
- Get a free API key if your application needs HTML, URL, or template input without running Chromium, installing fonts, or maintaining PDF workers.
ReportLab PDF generation is the right choice when you need direct Python control over PDF objects, but a hosted renderer becomes practical when documents depend on HTML, CSS, browser rendering, fonts, and production infrastructure. A local ReportLab script can create a PDF in a few lines, while PDFGeny provides a hosted rendering API with a median render time of 0.6 seconds and a single endpoint: POST https://pdfgeny.com/api/v1/render.
The decision is not about which tool creates a PDF file. Both approaches do. The decision is about where the complexity lives: inside your application stack, or inside a dedicated rendering service.
ReportLab PDF generation vs API rendering: choose based on document complexity
ReportLab works best for programmatic documents
ReportLab is a Python library that builds PDFs by drawing text, shapes, tables, and images directly into the PDF format. It avoids browser dependencies and gives developers precise control over placement.
A simple invoice generator can look like this:
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
pdf = canvas.Canvas("invoice.pdf", pagesize=letter)
pdf.drawString(72, 720, "Invoice #1001")
pdf.drawString(72, 690, "Customer: Example Company")
pdf.drawString(72, 660, "Amount: $250.00")
pdf.save()
The failure mode is layout drift: manually positioned PDFs become difficult to maintain when invoices gain dynamic tables, long addresses, translated text, or branding changes.
The fix is usually either adding higher-level ReportLab components such as Platypus or moving HTML-based layouts into a browser renderer.
Browser rendering changes the PDF workflow
PDFGeny accepts HTML, URLs, or templates and renders documents through headless Chromium. The service also supports WeasyPrint and Ghostscript for PDF/A-2b output.
For teams already producing web pages, the HTML-to-PDF path often matches existing frontend workflows. The internal PDFGeny template library contains 40 ready document templates covering common business documents such as invoices, receipts, contracts, certificates, reports, and labels.
Developers working with Python PDF libraries can compare local generation approaches with API approaches in this guide: Python PDF Library: Production API Guide for Developers.
Production PDF failures are usually infrastructure failures, not PDF failures
Cold Chromium startup creates latency spikes
The failure mode is cold browser startup latency. A headless Chromium process started for every request can add seconds before any HTML rendering begins.
A supplied production observation shows that a cold headless Chromium instance costs about 7.8 seconds per request, while keeping the browser warm brings rendering close to 0.65 seconds. The difference comes from process startup, browser initialization, and resource loading.
Chromium lifecycle management becomes part of your application if you run Puppeteer, Playwright, or custom containers yourself.
Web fonts fail quietly
The failure mode is font fallback. PDFs may contain a different typeface than the browser preview because rendering completes before external web fonts finish loading.
This issue affects both local Chromium deployments and APIs that accept external URLs. Developers need deterministic font loading, embedded assets, or renderer controls that wait for page resources.
PDFGeny removes the need to install Chromium binaries and fonts inside your own production containers by handling rendering infrastructure outside the application.
The API trade-off: operational simplicity costs control
Hosted rendering removes maintenance work
| Approach | Control | Operational work | Best fit |
|---|---|---|---|
| ReportLab | Direct PDF object control | Application owns layout logic and deployment | Small document sets and custom drawings |
| Puppeteer or Playwright | Browser-level control | Application owns Chromium lifecycle | Teams already operating browser automation |
| PDFGeny API | HTML, URL, templates, rendering options | Renderer infrastructure handled externally | Apps generating business documents |
The failure mode is over-engineering a low-volume PDF workflow. A developer generating a few documents per day may spend more time maintaining an API integration than maintaining a small ReportLab script.
That is the contrarian point: an API is not automatically better. For a handful of PDFs a day, a local library often wins because there is no network dependency, no external service boundary, and no per-document cost.
Pricing changes the calculation
| PDFGeny item | Value |
|---|---|
| Free plan | 50 documents/month, no card required |
| Overage | $0.009 per document |
| Batch processing | Up to 100 documents in one call |
| Rendering endpoint | POST https://pdfgeny.com/api/v1/render |
Developers comparing options should calculate document volume, engineering time, and infrastructure ownership rather than only comparing package installation costs.
Code examples: calling a PDF generation API from five stacks
Python
The failure mode is framework blocking. Running sync_playwright directly inside Django can raise SynchronousOnlyOperation unless rendering runs on its own thread.
A direct API call avoids embedding browser execution:
import requests
response = requests.post(
"https://pdfgeny.com/api/v1/render",
json={
"html": "<h1>Invoice 1001</h1>",
"format": "pdf"
},
headers={
"Authorization": "Bearer YOUR_API_KEY"
}
)
with open("invoice.pdf", "wb") as file:
file.write(response.content)
Node.js
const response = await fetch(
"https://pdfgeny.com/api/v1/render",
{
method: "POST",
headers: {
"Authorization": "Bearer YOUR_API_KEY",
"Content-Type": "application/json"
},
body: JSON.stringify({
html: "<h1>Receipt</h1>"
})
}
);
const buffer = await response.arrayBuffer();
await Bun.write("receipt.pdf", buffer);
PHP
<?php
$ch = curl_init("https://pdfgeny.com/api/v1/render");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer YOUR_API_KEY",
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
"html" => "<h1>Contract</h1>"
]));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$pdf = curl_exec($ch);
file_put_contents("contract.pdf", $pdf);
curl_close($ch);?>
Go
package main
import (
"bytes"
"fmt"
"net/http"
)
func main() {
body:= bytes.NewBufferString(`{"html":"<h1>Report</h1>"}`)
req, _:= http.NewRequest(
"POST",
"https://pdfgeny.com/api/v1/render",
body,
)
req.Header.Set("Authorization", "Bearer YOUR_API_KEY")
req.Header.Set("Content-Type", "application/json")
client:= &http.Client{}
resp, _:= client.Do(req)
fmt.Println(resp.StatusCode)
}
Ruby
require "net/http"
require "json"
uri = URI("https://pdfgeny.com/api/v1/render")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = {
html: "<h1>Certificate</h1>"
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(request)
end
File.binwrite("certificate.pdf", response.body)
cURL
curl -X POST https://pdfgeny.com/api/v1/render \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Invoice</h1>"}' \
--output invoice.pdf
Developers converting HTML documents can also review How to Save HTML as PDF: API and Code Guide and Chrome HTML Document to PDF: Real Costs and Production Issues.
Security and delivery problems appear before rendering
URL-to-PDF endpoints create SSRF risks
The failure mode is server-side request forgery. A URL-to-PDF feature can become a network access path if it fetches internal addresses.
A URL renderer must resolve the host and reject private, loopback, and metadata addresses before requesting the page.
The risk applies to custom browser deployments and hosted services. Any system fetching user-provided URLs needs URL validation, DNS checks, and network restrictions.
Async jobs change document workflows
The failure mode is long-running request timeouts. Large reports, batches, or complex pages can exceed normal HTTP request limits.
PDFGeny supports sync and async jobs, signed HMAC webhooks, and stored documents so applications can separate user requests from document completion.
What We Got Wrong / What Surprised Us
The strongest non-obvious lesson is that PDF generation problems rarely begin with PDF syntax. They begin with browsers, fonts, networking, and deployment decisions.
The common assumption is that replacing wkhtmltopdf with Chromium automatically solves everything. The failure mode is renderer migration without CSS review. wkhtmltopdf is not dead; it is unmaintained, and the real cost is often the CSS features it never learned.
The surprising trade-off is that a simpler library can beat an API for tiny workloads. ReportLab remains valuable because it removes entire categories of infrastructure. The complexity appears when documents start behaving like web pages rather than static drawings.
Practical Takeaways
- Measure your document volume first.
Expected outcome: a clearer build-versus-buy decision. Time estimate: 30 minutes. Difficulty: Easy.
- Separate layout from rendering.
Expected outcome: fewer invoice and report changes breaking application code. Time estimate: 1-2 days. Difficulty: Medium.
- Audit URL rendering security.
Expected outcome: reduced SSRF exposure. Time estimate: 2-4 hours. Difficulty: Medium.
- Test fonts and page breaks before production.
Expected outcome: fewer customer-facing PDF defects. Time estimate: 1 day. Difficulty: Easy.
- Choose ReportLab for small controlled output and an API for operationally heavy rendering.
Expected outcome: lower maintenance cost aligned with workload. Time estimate: 1 hour evaluation. Difficulty: Easy.
Try PDFGeny for production PDF workflows
Applications that need invoices, receipts, contracts, certificates, reports, or labels can send HTML, a URL, or a template and receive a finished PDF without running Chromium containers or managing font installations. PDFGeny provides a free plan with 50 documents per month and no card requirement.
Start with a free API key and test your own document flow before committing engineering time to browser infrastructure.
FAQ
Is ReportLab still used for PDF generation?
Yes. ReportLab remains useful for Python applications that need direct control over PDF drawing operations. It becomes less convenient when documents require complex HTML layouts, CSS, and browser-like rendering.
What is the fastest way to generate PDFs from HTML?
A browser renderer is usually the closest match for HTML and CSS documents. PDFGeny uses headless Chromium as the default engine and reports a median render time of 0.6 seconds.
Is wkhtmltopdf still a good production choice?
wkhtmltopdf can still work for existing systems, but its maintenance status means teams should consider the CSS requirements of new projects before adopting it.
When should a developer choose a PDF API instead of a library?
A PDF API is useful when document generation requires managed rendering infrastructure, async workflows, templates, webhooks, or multiple application languages. A local library is often better for low-volume, self-contained document creation.