Invalid Date

"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.

Try it free →

Related

Powered by FUNBREW PDF