"Should I stick with wkhtmltopdf, or migrate to Chromium?" is one of the most common questions developers ask when building PDF generation into their applications.
This article breaks down the differences with concrete benchmarks, API call examples, and use-case-specific workflows to help you make the right choice.
What is wkhtmltopdf?
wkhtmltopdf is an open-source HTML to PDF converter based on the QtWebKit engine. It's been around since 2012 and is used in countless projects.
Strengths
- Fast: 2–5x faster than Chromium
- Lightweight: Low memory footprint (~50–100MB)
- Simple: CLI tool, no setup complexity
- Battle-tested: Years of production use
Limitations
- Incomplete CSS Grid / Flexbox support
- Limited JavaScript execution
- Based on an outdated WebKit version
- Maintenance has stalled
What is Chromium-based PDF Generation?
Tools like Puppeteer, Playwright, and Gotenberg use Chromium (the open-source engine behind Google Chrome) to render HTML and produce PDFs.
Strengths
- Full CSS support: Grid, Flexbox, CSS variables, calc() — everything works
- JavaScript execution: Charts, SPAs, dynamic content
- Latest web standards: Same rendering as a real browser
- @media print: Print stylesheets render correctly
Limitations
- Slow cold start (1–3 seconds)
- High memory usage (200–500MB)
- Requires Docker or process management
Quality Comparison
CSS Support
<!-- This layout breaks in wkhtmltopdf -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 16px;">
<div>Left column</div>
<div>Right column</div>
</div>
| CSS Feature | wkhtmltopdf | Chromium |
|---|---|---|
| Flexbox | Partial | Full |
| CSS Grid | No | Full |
| CSS Variables | No | Full |
| calc() | Partial | Full |
| @media print | Partial | Full |
| Web Fonts | Manual setup | Auto-loaded |
| WebP / AVIF images | No | Full |
Image Format Support
JPEG, PNG and GIF render correctly on both engines. Modern formats like WebP and AVIF are not supported by wkhtmltopdf (QtWebKit) and will be missing from the output PDF. Chromium renders them natively since it's a full modern browser.
<!-- Won't render in wkhtmltopdf (Chromium only) -->
<img src="photo.webp" alt="Photo">
<!-- Works in both engines -->
<img src="photo.jpg" alt="Photo">
When passing HTML with WebP images through FUNBREW PDF, specify "engine": "quality" to ensure correct rendering.
Speed
For simple HTML documents (measured):
| Content | wkhtmltopdf | Chromium |
|---|---|---|
| 1 page text | 0.3s | 1.2s |
| Table with 10 rows | 0.5s | 1.5s |
| 5 pages with images | 1.2s | 2.8s |
| CSS-rich report | 1.5s | 2.5s |
wkhtmltopdf is consistently faster, but the gap may be acceptable depending on your use case.
When to Use Which
Use wkhtmltopdf when:
- Simple text/table PDFs (receipts, basic invoices)
- High volume (thousands per month)
- Low latency required (real-time generation)
- Limited server resources
Use Chromium when:
- Design-heavy documents (CSS Grid/Flexbox layouts)
- Charts or dynamic content (JavaScript rendering)
- URL-to-PDF conversion (capture a live web page)
- Print CSS accuracy matters
API Call Examples
Here's how to switch engines using the FUNBREW PDF API in common languages.
cURL
# Fast engine (wkhtmltopdf) — speed-first
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<h1>Invoice</h1><p>Total: $100</p>",
"options": {
"engine": "fast",
"format": "A4"
}
}' \
-o invoice.pdf
# Quality engine (Chromium) — CSS Grid or JavaScript required
curl -X POST https://pdf.funbrew.cloud/api/v1/pdf/from-html \
-H "Authorization: Bearer $FUNBREW_PDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"html": "<div style=\"display:grid;grid-template-columns:1fr 1fr\">...</div>",
"options": {
"engine": "quality",
"format": "A4"
}
}' \
-o report.pdf
Python
import httpx
import os
FUNBREW_PDF_API_KEY = os.environ["FUNBREW_PDF_API_KEY"]
def generate_pdf(html: str, engine: str = "fast") -> bytes:
"""
engine: "fast" (wkhtmltopdf) or "quality" (Chromium)
"""
response = httpx.post(
"https://pdf.funbrew.cloud/api/v1/pdf/from-html",
headers={
"Authorization": f"Bearer {FUNBREW_PDF_API_KEY}",
"Content-Type": "application/json",
},
json={
"html": html,
"options": {
"engine": engine,
"format": "A4",
},
},
timeout=30.0,
)
response.raise_for_status()
return response.content
# Invoice: simple HTML → fast engine is sufficient
invoice_html = "<h1>Invoice</h1><table>...</table>"
pdf_bytes = generate_pdf(invoice_html, engine="fast")
with open("invoice.pdf", "wb") as f:
f.write(pdf_bytes)
# Report with charts: CSS Grid → quality engine required
report_html = """
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
<div><canvas id="chart1"></canvas></div>
<div><canvas id="chart2"></canvas></div>
</div>
"""
pdf_bytes = generate_pdf(report_html, engine="quality")
with open("report.pdf", "wb") as f:
f.write(pdf_bytes)
Node.js
const fs = require('fs');
async function generatePdf(html, engine = 'fast') {
const response = await fetch('https://pdf.funbrew.cloud/api/v1/pdf/from-html', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.FUNBREW_PDF_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
html,
options: {
engine, // 'fast' (wkhtmltopdf) or 'quality' (Chromium)
format: 'A4',
},
}),
});
if (!response.ok) {
throw new Error(`PDF generation failed: ${response.statusText}`);
}
return Buffer.from(await response.arrayBuffer());
}
async function main() {
// Receipt: simple HTML → fast
const receiptHtml = '<h1>Receipt</h1><p>$50.00</p>';
const receiptPdf = await generatePdf(receiptHtml, 'fast');
fs.writeFileSync('receipt.pdf', receiptPdf);
// Monthly report: Chart.js → quality
const reportHtml = `
<html>
<head><script src="https://cdn.jsdelivr.net/npm/chart.js"></script></head>
<body>
<h1>Monthly Report</h1>
<canvas id="salesChart" width="400" height="200"></canvas>
<script>
new Chart(document.getElementById('salesChart'), {
type: 'bar',
data: { labels: ['Jan','Feb','Mar'], datasets: [{ data: [65,59,80] }] }
});
</script>
</body>
</html>
`;
const reportPdf = await generatePdf(reportHtml, 'quality');
fs.writeFileSync('report.pdf', reportPdf);
}
main();
Use-Case Workflows
Invoice Generation (wkhtmltopdf / fast)
Invoices are typically text and tables — wkhtmltopdf delivers sufficient quality at high speed.
Order confirmed event
↓
Build invoice data as JSON
↓
generate_pdf(html, engine="fast") ← completes in 0.3–0.5s
↓
Upload to S3 + send email attachment
See automate invoice PDF generation for a full implementation example, and invoice automation use cases for real-world patterns.
Monthly Report (Chromium / quality)
When charts or dashboard screenshots are required, Chromium is essential.
End-of-month batch job
↓
Aggregate data → build chart HTML (Chart.js etc.)
↓
generate_pdf(html, engine="quality") ← 2–3s, quality over speed
↓
Save to dashboard + distribute to managers
See report automation use cases for real examples with charts.
Certificate / Diploma (Chromium / quality)
Certificates with decorative fonts and complex layouts require Chromium for accurate rendering.
Completion event (exam passed, training finished, etc.)
↓
Inject student name + completion date into template
↓
generate_pdf(html, engine="quality") ← Flexbox layout supported
↓
Email PDF to recipient + save to user profile
See certificate automation use cases for implementation patterns.
High-Volume Batch Generation (wkhtmltopdf / fast)
For thousands of documents (monthly statements, bulk delivery notifications), throughput matters most.
import asyncio
import httpx
async def batch_generate(records: list[dict]) -> list[bytes]:
"""Parallel generation with fast engine for high throughput"""
async with httpx.AsyncClient() as client:
tasks = [
client.post(
"https://pdf.funbrew.cloud/api/v1/pdf/from-html",
headers={"Authorization": f"Bearer {API_KEY}"},
json={"html": build_html(r), "options": {"engine": "fast"}},
timeout=30.0,
)
for r in records
]
responses = await asyncio.gather(*tasks)
return [r.content for r in responses]
Dual Engine with FUNBREW PDF
FUNBREW PDF lets you choose the engine per API request:
{
"html": "<h1>Hello</h1>",
"options": {
"engine": "fast"
}
}
| Parameter | Engine | Best for |
|---|---|---|
"engine": "fast" |
wkhtmltopdf | Speed, simple HTML |
"engine": "quality" |
Chromium/Gotenberg | Quality, modern CSS |
Switch engines with a single parameter — same template, different output quality.
Engine Availability by Plan
| Plan | Fast (wkhtmltopdf) | Quality (Chromium) |
|---|---|---|
| Free | Available | Playground only |
| Starter | Available | Playground only |
| Basic+ | Available | Available |
Migration Cost Reality: wkhtmltopdf to Chromium
Understanding the actual migration cost is essential before switching engines.
Key Migration Challenges
1. Infrastructure changes
wkhtmltopdf runs as a single binary. Chromium via Puppeteer/Playwright requires a Node.js runtime and a 200–500MB Chromium binary. Docker image sizes grow significantly and cold starts become slower.
# wkhtmltopdf-based Docker image (~180MB)
FROM surnet/alpine-wkhtmltopdf:3.1.0-0.12.6-full
# Puppeteer-based image (~800MB)
FROM node:20-slim
RUN apt-get update && apt-get install -y chromium
2. Operational complexity
Chromium requires sandbox configuration, memory management, and process monitoring. The PDF API production guide covers Chromium-specific considerations in detail, including --no-sandbox configuration and concurrency limits.
3. Cost comparison
| Factor | wkhtmltopdf | Chromium |
|---|---|---|
| Infrastructure | Low (small memory) | High (500MB+) |
| Dev & maintenance | Low (simple) | High (Chromium management) |
| Version pinning | Rarely needed | Required (Chrome updates monthly) |
| Security patches | Few times/year | Monthly Chrome updates |
To avoid this complexity, a managed PDF API service is a rational alternative. See Puppeteer to FUNBREW PDF migration for how to replace self-hosted Chromium with an API call.
Engine-Specific Troubleshooting
Common Problems and Fixes
Problem 1: Japanese/CJK characters are garbled in wkhtmltopdf
wkhtmltopdf lacks font auto-detection. If Japanese fonts aren't installed, output will be blank boxes.
# Install CJK fonts on Ubuntu
apt-get install fonts-noto-cjk
# Explicitly set encoding
wkhtmltopdf --encoding utf-8 input.html output.pdf
See the Japanese PDF font guide for full setup details. Chromium reads system fonts automatically, so this issue is less common.
Problem 2: CSS Grid layouts break in wkhtmltopdf
CSS Grid is not supported in wkhtmltopdf. Use the table-based workaround:
/* Only works in Chromium */
.report-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
/* Compatible with wkhtmltopdf */
.report-table { width: 100%; table-layout: fixed; }
.report-cell { width: 33.3%; vertical-align: top; padding: 12px; }
More workarounds in the HTML to PDF CSS optimization guide.
Problem 3: Page breaks are wrong in Chromium
.section { page-break-before: always; }
.no-break { page-break-inside: avoid; }
See PDF output troubleshooting for a systematic approach to diagnosing page break issues.
Engine Status in 2026
wkhtmltopdf Maintenance Status
wkhtmltopdf is effectively unmaintained. Commits to the GitHub repository have nearly stopped. Based on the aging QtWebKit engine, there is no path to CSS Grid, WebP image, or modern JavaScript support.
We do not recommend adopting wkhtmltopdf for new projects. For existing systems, continued use is acceptable, but plan to migrate when adding features that require modern CSS.
Chromium / Puppeteer Trends
Since Puppeteer v22 (2024+), integration with Chrome for Testing allows pinning specific Chrome versions, preventing unexpected rendering changes from browser updates.
// Puppeteer v22+: use Chrome for Testing to pin a version
const browser = await puppeteer.launch({
executablePath: '/path/to/chrome-for-testing',
args: ['--no-sandbox', '--disable-dev-shm-usage'],
});
Managed API as a Third Option
Rather than operating wkhtmltopdf or Chromium yourself, a managed API like FUNBREW PDF handles engine installation, updates, and security patches. The "engine" parameter in the API lets you switch between fast (wkhtmltopdf-equivalent) and quality (Chromium-equivalent) without any infrastructure changes.
For a cost comparison between self-hosted and managed, see PDF API pricing comparison.
Error Handling Differences
wkhtmltopdf and Chromium fail differently. See the PDF API error handling guide for retry strategies that work across both engines.
| Error | wkhtmltopdf | Chromium |
|---|---|---|
| Missing font | Silent failure (blank boxes) | Console warning |
| JS error | Silently ignored | Can throw |
| Timeout | Process hangs | Configurable via page.goto() |
| OOM | Crash | OOM-killed |
Summary
- Speed-first, simple HTML → wkhtmltopdf (
fast) - Quality-first, modern CSS → Chromium (
quality) - Want both → FUNBREW PDF's dual engine approach
- New projects → Avoid wkhtmltopdf; use Chromium or a managed API
Start with fast and switch to quality if rendering breaks. That's the most pragmatic approach.
Related
- HTML to PDF API Comparison 2026 — Compare pricing, features across services
- Puppeteer to PDF API Migration — Cut Chromium operational overhead
- PDF API Quickstart — Node.js, Python, and PHP code examples
- PDF Template Engine Guide — Efficient PDF generation with templates
- PDF API Production Guide — Chromium production configuration
- PDF API Error Handling — Engine-specific error patterns
- HTML to PDF CSS Tips — CSS settings for both engines
- PDF API Pricing Comparison — Self-hosted vs managed cost analysis
- Invoice Automation Use Case — Invoice generation patterns
- Report Automation Use Case — Charts and report generation examples
- Certificate Automation Use Case — Automated certificate issuance
- Playground — Try both engines in your browser