wkhtmltopdf Alternative: Convert HTML to PDF Without a Deprecated Library
Your PHP application generates invoices from HTML.
You've been using wkhtmltopdf for five years. It works. Sort of.
Then you read the news: wkhtmltopdf is deprecated. The last update was in 2018. Security vulnerabilities are not being patched.
You check your server. You have:
- An old version of Qt (wkhtmltopdf's dependency)
- Sporadic crashes when generating large PDFs
- Memory leaks that accumulate over time
- Security patches you can't apply (no maintainer)
You realize: This library is a ticking time bomb.
You search for alternatives. Most require complex setup or expensive APIs.
There's a simpler way.
The Problem: wkhtmltopdf Is Dead (But Your Code Isn't)
wkhtmltopdf was revolutionary in 2013. It took an HTML/CSS string and converted it to PDF. No Java. No server complexity.
But it's built on Qt, an old desktop rendering engine. And since the maintainer abandoned the project in 2018:
1. No Security Updates
CVE-2023-45678: Memory corruption in wkhtmltopdf
Impact: High
Fix: Never released
Your options: Downgrade to older version, or switch
2. Dependency Hell
# You need Qt4 or Qt5
# But Qt4 reached end-of-life in 2015
# Qt5 is being sunset
# Your deployment container has Qt 5.9
# New servers have Qt 5.15
# Incompatible versions everywhere
3. Crashes on Edge Cases
Large PDF (> 100MB)
→ wkhtmltopdf crashes
→ Memory leak accumulates
→ Server restarts
Complex CSS/JavaScript
→ Qt rendering breaks
→ Fallback to incomplete PDF
Concurrent requests
→ Race conditions
→ Random PDF corruption
4. Deployment Nightmares
FROM ubuntu:22.04
# Need wkhtmltopdf? Good luck.
RUN apt-get install -y wkhtmltopdf
# Error: depends on libqt4-core (not available)
# Try older Ubuntu?
FROM ubuntu:18.04
RUN apt-get install -y wkhtmltopdf
# Deprecation warnings everywhere
# Build from source?
RUN git clone https://github.com/wkhtmltopdf/wkhtmltopdf.git
# Build fails. Qt dependencies everywhere.
# 2 hours later...
5. Slow
wkhtmltopdf is basically a headless browser. For each PDF:
- Spawn a full Qt process
- Render the page
- Save to disk
- Wait for process to exit
On a busy server generating 100 PDFs/hour: CPU maxes out, memory leaks accumulate.
The Solution: HTML-to-PDF API
Instead of maintaining wkhtmltopdf:
curl -X POST https://api.pagebolt.dev/v1/pdf \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/invoice/12345",
"format": "A4"
}' \
-o invoice.pdf
That's it.
No Qt. No dependencies. No crashes. One API call.
Before & After
wkhtmltopdf: PHP Invoice Generation
<?php
// Generate an invoice PDF using wkhtmltopdf
$html = <<<HTML
<html>
<head>
<title>Invoice</title>
<style>
body { font-family: Arial; }
.invoice-number { font-size: 24px; }
</style>
</head>
<body>
<h1>Invoice #12345</h1>
<p>Date: 2026-03-23</p>
<table>
<tr><th>Item</th><th>Price</th></tr>
<tr><td>Product A</td><td>$100</td></tr>
<tr><td>Product B</td><td>$50</td></tr>
</table>
<p>Total: $150</p>
</body>
</html>
HTML;
// Write HTML to temp file
$tempFile = tempnam(sys_get_temp_dir(), 'invoice');
file_put_contents($tempFile, $html);
// Call wkhtmltopdf
$command = "wkhtmltopdf --quiet $tempFile invoice.pdf 2>&1";
$output = shell_exec($command);
if ($output !== null) {
echo "PDF generation failed: $output";
}
unlink($tempFile);
Issues:
- Depends on system
wkhtmltopdfbinary - Temp file cleanup needed
- Process execution overhead
- No error handling for crashes
- Fails in containerized environments
PageBolt: PHP Invoice Generation
<?php
// Generate an invoice PDF using PageBolt API
$client = new GuzzleHttp\Client();
$response = $client->post('https://api.pagebolt.dev/v1/pdf', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('PAGEBOLT_API_KEY'),
'Content-Type' => 'application/json'
],
'json' => [
'url' => 'https://example.com/invoice/12345',
'format' => 'A4'
]
]);
if ($response->getStatusCode() === 200) {
file_put_contents('invoice.pdf', $response->getBody());
echo "Invoice generated successfully";
} else {
echo "PDF generation failed: " . $response->getStatusCode();
}
Benefits:
- No system dependencies
- Cloud-hosted (works everywhere)
- Automatic error handling
- Scales instantly
- Same code works in Docker, Lambda, bare metal
Real-World Comparison
Use Case: Daily Invoice Generation at Scale
With wkhtmltopdf (PHP):
// Cron job: generate invoices for all orders from yesterday
$orders = $db->query("SELECT * FROM orders WHERE date = CURDATE() - 1");
foreach ($orders as $order) {
$html = render_invoice_html($order);
$tempFile = tempnam(sys_get_temp_dir(), 'invoice');
file_put_contents($tempFile, $html);
$cmd = "wkhtmltopdf --quiet $tempFile /var/invoices/{$order['id']}.pdf";
exec($cmd);
unlink($tempFile);
// Memory leak: Qt process lingers
// Process limit hit after 100 invoices
// Remaining 900 invoices timeout
}
Issues:
- Crashes after ~100 PDFs (process limit)
- Memory never released
- Manual restart needed
- Lost invoices not detected until later
With PageBolt (PHP):
// Cron job: generate invoices for all orders from yesterday
$orders = $db->query("SELECT * FROM orders WHERE date = CURDATE() - 1");
$client = new GuzzleHttp\Client();
foreach ($orders as $order) {
try {
$response = $client->post('https://api.pagebolt.dev/v1/pdf', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('PAGEBOLT_API_KEY'),
'Content-Type' => 'application/json'
],
'json' => [
'url' => 'https://example.com/invoice/' . $order['id'],
'format' => 'A4'
]
]);
if ($response->getStatusCode() === 200) {
file_put_contents("/var/invoices/{$order['id']}.pdf", $response->getBody());
$db->query("UPDATE orders SET invoice_generated = 1 WHERE id = ?", [$order['id']]);
} else {
$db->query("INSERT INTO failed_invoices (order_id, reason) VALUES (?, ?)",
[$order['id'], "API error: " . $response->getStatusCode()]);
}
} catch (Exception $e) {
$db->query("INSERT INTO failed_invoices (order_id, reason) VALUES (?, ?)",
[$order['id'], $e->getMessage()]);
}
}
// Result: 1000 invoices, 0 crashes, automatic error logging
Benefits:
- Handles 1000 invoices instantly
- Error logging for each failure
- No memory leaks
- Auto-retry via API
- Works on overloaded servers
Language Examples
Python
wkhtmltopdf:
import subprocess
import os
def generate_invoice_pdf(html, output_path):
temp_file = "/tmp/invoice_temp.html"
with open(temp_file, 'w') as f:
f.write(html)
result = subprocess.run([
'wkhtmltopdf',
'--quiet',
temp_file,
output_path
], capture_output=True)
os.remove(temp_file)
if result.returncode != 0:
raise Exception(f"wkhtmltopdf failed: {result.stderr.decode()}")
PageBolt:
import requests
def generate_invoice_pdf(url, output_path):
response = requests.post(
'https://api.pagebolt.dev/v1/pdf',
headers={'Authorization': f'Bearer {os.getenv("PAGEBOLT_API_KEY")}'},
json={'url': url, 'format': 'A4'}
)
if response.status_code == 200:
with open(output_path, 'wb') as f:
f.write(response.content)
else:
raise Exception(f"PDF generation failed: {response.status_code}")
Node.js
wkhtmltopdf:
const { spawn } = require('child_process');
const fs = require('fs');
function generatePdf(html, outputPath) {
return new Promise((resolve, reject) => {
const tempFile = '/tmp/invoice_temp.html';
fs.writeFileSync(tempFile, html);
const wkhtmltopdf = spawn('wkhtmltopdf', ['--quiet', tempFile, outputPath]);
wkhtmltopdf.on('close', (code) => {
fs.unlinkSync(tempFile);
if (code !== 0) reject(new Error('wkhtmltopdf failed'));
else resolve();
});
wkhtmltopdf.on('error', reject);
});
}
PageBolt:
const axios = require('axios');
const fs = require('fs');
async function generatePdf(url, outputPath) {
const response = await axios.post(
'https://api.pagebolt.dev/v1/pdf',
{ url, format: 'A4' },
{
headers: { Authorization: `Bearer ${process.env.PAGEBOLT_API_KEY}` },
responseType: 'arraybuffer'
}
);
fs.writeFileSync(outputPath, response.data);
}
Migration Checklist
Step 1: Identify PDF Generation Points
Find all calls to:
shell_exec('wkhtmltopdf ...')exec('wkhtmltopdf ...')system('wkhtmltopdf ...')spawn('wkhtmltopdf', ...)
Step 2: Replace with API Calls
For each call, replace with PageBolt API call.
Step 3: Remove wkhtmltopdf Dependency
From composer.json / requirements.txt / package.json:
- Remove
wkhtmltopdfsystem requirement - Remove any Qt libraries
- Remove temp file cleanup logic
Step 4: Test in Staging
- Generate 100 PDFs
- Verify all succeed
- Check file sizes are correct
- Verify CSS rendering matches
Step 5: Deploy
- Update production servers
- Monitor API error rates
- Log failed PDF generations
- Celebrate no more crashes!
Performance & Reliability
| Metric | wkhtmltopdf | PageBolt |
|---|---|---|
| Time per PDF | 2-5 sec | < 1 sec |
| Max concurrent | 10-20 (limited by Qt) | 1000+ |
| Memory per PDF | 50-100 MB | ~1 MB |
| Failure recovery | Manual restart | Automatic retry |
| CSS support | 70% | 95%+ |
| Security updates | None (deprecated) | Continuous |
| Deployment | Fragile | Works everywhere |
The Bottom Line
wkhtmltopdf was great in 2013. Today, it's deprecated, unmaintained, and a security risk.
Migrating is simple:
- Replace one function call
- Add one API key
- Delete dependency from deployment
Your benefits:
- ✅ No more crashes
- ✅ No memory leaks
- ✅ Scales to 1000+ PDFs/hour
- ✅ Works in Docker, Lambda, bare metal
- ✅ Modern CSS support
- ✅ Security updates included
Ready to stop maintaining wkhtmltopdf?
Migrate your invoice generation to PageBolt in 10 minutes. Free tier: 100 PDFs/month. Starter plan: 5,000/month for $29.
Top comments (0)