Comparisons

wkhtmltopdf: The Unmaintained Renderer's Real Costs and Alternatives

wkhtmltopdf is unmaintained, not dead. Understand its real costs in CSS rendering, memory, and cold starts, and compare with headless Chromium APIs.

M Mikel Rougstone · 2 September 2026 · 8 min read
wkhtmltopdf: The Unmaintained Renderer's Real Costs and Alternatives
  • wkhtmltopdf is an unmaintained tool; its core cost is its dated WebKit engine, lacking modern CSS support.
  • A cold headless Chromium instance incurs a ~7.8 s latency, reduced to ~0.65 s when kept warm.
  • Maintaining wkhtmltopdf or Chromium involves managing memory leaks, web font rendering issues, and SSRF vulnerabilities for URL inputs.
  • For low-volume needs (a few documents daily), a local library often outperforms an API in cost and latency.
  • PDFGeny offers headless Chromium rendering at a median of 0.6 s per document, with a free plan for 50 documents a month.

Get a free API key

wkhtmltopdf remains a common tool for converting HTML to PDF, but its unmaintained status introduces significant technical debt, especially concerning CSS rendering. The real cost of wkhtmltopdf is not its initial setup, but its inability to parse modern CSS, leading to unexpected layout shifts and rendering inconsistencies compared to contemporary browser engines like headless Chromium. This gap in rendering capability means developers spend more time debugging CSS workarounds than on core application logic.

The Unmaintained Reality of wkhtmltopdf

wkhtmltopdf is not 'dead'; it is unmaintained. This distinction is crucial. A dead project might be abandoned entirely, but an unmaintained one implies a functional baseline that simply doesn't evolve. The core issue with wkhtmltopdf stems from its reliance on an older WebKit rendering engine, which has not seen updates to support modern CSS features like Flexbox, Grid, or even advanced pseudo-elements. For example, rendering a complex invoice with modern layout directives will often produce a broken PDF, requiring developers to revert to older, less efficient CSS techniques or implement extensive manual styling adjustments specifically for wkhtmltopdf. The cost isn't licensing; it's developer time spent on compatibility.

CSS Inconsistencies and Web Font Failures

The limitations of wkhtmltopdf's WebKit engine manifest clearly in CSS rendering. Features taken for granted in modern browsers—like display: flex for responsive layouts or grid-template-areas for intricate page structures—are often ignored or rendered incorrectly. This forces developers to write conditional CSS or fallback to table-based layouts, significantly increasing development complexity and maintenance burden.

A more subtle, yet common, failure mode for wkhtmltopdf and even some headless browser setups involves web fonts. Web fonts silently fall back in PDFs when the renderer finishes before the font loads. This is particularly noticeable in situations with slow network conditions or large font files. The PDF is generated, but with a system font, leading to visual inconsistencies that are hard to debug without careful inspection of timing and network activity during the rendering process. Ensuring font availability and proper loading before rendering is a critical, often overlooked, detail.

Performance and Resource Management in PDF Generation

Generating PDFs, especially with headless browsers, demands careful resource management. A cold headless Chromium instance costs about 7.8 seconds per request for initialization. This latency is unacceptable for user-facing applications requiring immediate PDF delivery, such as receipts or tickets. Keeping the browser warm, however, brings this latency down significantly, to approximately 0.65 seconds per request. This difference highlights the operational overhead of self-hosting a headless browser solution.

Memory Leaks and Cold Starts

Headless Chromium, while powerful, is prone to memory leaks if not managed carefully. Long-running processes can accumulate memory, eventually leading to performance degradation or crashes. This necessitates a robust orchestration layer, often involving containerization (e.g., Docker) and process supervision (e.g., Kubernetes liveness probes) to restart instances and reclaim memory. A typical setup might involve monitoring memory usage and restarting a Chromium container if it exceeds a predefined threshold, say 2GB, to prevent system instability.

Cold starts are another major concern. The 7.8-second penalty for a cold headless Chromium instance makes it unsuitable for synchronous API calls where users expect immediate responses. Solutions involve pre-warming instances, which consumes compute resources even when idle, or using serverless functions with provisioned concurrency, which comes at a higher cost. For a service like PDFGeny, which achieves a median render time of 0.6 seconds, extensive infrastructure is dedicated to keeping Chromium instances warm and ready, distributing the load across multiple machines.

Security Implications of URL-to-PDF Conversion

A URL-to-PDF endpoint, while convenient, presents a significant security vulnerability: Server-Side Request Forgery (SSRF). An attacker could supply a malicious URL pointing to internal network resources, metadata endpoints (e.g., AWS EC2 metadata service at http://169.254.169.254/latest/meta-data/), or other sensitive services.

Mitigating SSRF Risks

Before rendering any URL-supplied content, the host must be resolved, and private, loopback, and metadata addresses must be explicitly rejected. This involves:

  • DNS Resolution: Resolve the domain to an IP address.
  • IP Address Validation: Check the resolved IP against a deny-list of private IP ranges (RFC 1918), loopback addresses (127.0.0.1/8), and cloud metadata service IPs.
  • Port Filtering: Optionally, restrict allowed ports to prevent connections to common internal service ports.

A robust implementation would parse the URL, perform DNS resolution, and then validate the resulting IP address against known unsafe ranges.

import ipaddress
import socket
from urllib.parse import urlparse

def is_private_ip(ip_str):
    private_ranges = [
        ipaddress.ip_network('10.0.0.0/8'),
        ipaddress.ip_network('172.16.0.0/12'),
        ipaddress.ip_network('192.168.0.0/16'),
        ipaddress.ip_network('127.0.0.0/8'), # Loopback
        ipaddress.ip_network('169.254.0.0/16') # Link-local & AWS metadata
    ]
    try:
        ip = ipaddress.ip_address(ip_str)
        for r in private_ranges:
            if ip in r:
                return True
        return False
    except ValueError:
        return True # Not a valid IP, treat as unsafe

def validate_url_for_ssrf(url):
    parsed_url = urlparse(url)
    hostname = parsed_url.hostname
    if not hostname:
        raise ValueError("Invalid URL: no hostname")

    try:
        # Resolve hostname to IP address
        ip_addresses = socket.gethostbyname_ex(hostname)[2]
        for ip_addr in ip_addresses:
            if is_private_ip(ip_addr):
                raise ConnectionRefusedError(f"SSRF detected: Private IP address {ip_addr} for {hostname}")
        return True
    except socket.gaierror:
        raise ConnectionRefusedError(f"Could not resolve hostname {hostname}")

# Example usage (would typically be called before passing URL to renderer)
# try:
#     validate_url_for_ssrf("http://192.168.1.100/internal-report")
# except ConnectionRefusedError as e:
#     print(e)
#
# try:
#     validate_url_for_ssrf("http://example.com/public-page")
# except ConnectionRefusedError as e:
#     print(e)

This Python snippet demonstrates a basic approach to IP validation. Without such checks, a PDF generation service becomes an immediate attack vector.

When a Local Library Beats an API

A contrarian, yet practical, observation is that for a handful of documents a day, a local library like wkhtmltopdf or even a self-hosted Puppeteer instance beats any API. The overhead of setting up an API call, managing API keys, and dealing with potential network latency might outweigh the benefits for extremely low-volume use cases. If an application generates, for example, 5-10 PDFs per day, the development effort to integrate an API might be similar to the effort of packaging wkhtmltopdf locally. The cost savings of avoiding API calls at $0.009 per document (after the free tier) are negligible for such low volumes. The primary benefit of an API like PDFGeny comes from offloading maintenance, scaling, and handling the complexities of headless browser management at scale.

What We Got Wrong / What Surprised Us

One significant learning was the subtle yet persistent issue of SynchronousOnlyOperation when running sync_playwright within a Django application. Initially, we attempted to integrate Playwright directly into Django's request-response cycle. However, Django's ORM and other components are designed for synchronous execution in the main thread. Running an asynchronous operation like sync_playwright (which internally uses async primitives) on the same thread as Django's synchronous ORM calls leads to this error, preventing database access or other synchronous operations. The fix was clear: the render process must happen on its own thread, separate from Django's main request handling. This often means offloading the PDF generation to a background task queue (e.g., Celery) or a separate microservice, adding architectural complexity. This constraint is not unique to Django; similar issues arise in other synchronous frameworks trying to integrate async I/O without proper threading or process isolation.

Practical Takeaways

  • Evaluate wkhtmltopdf's CSS limitations (Difficulty: Low, Time: 1 hour): Before committing to wkhtmltopdf, render a few complex HTML documents with modern CSS (Flexbox, Grid). If the output is visually broken, allocate time for extensive CSS rewrites or consider alternatives. Expected outcome: Clear understanding of rendering fidelity.
  • Implement SSRF protection for URL inputs (Difficulty: Medium, Time: 3-5 hours): If your application accepts URLs for PDF generation, implement robust IP address validation as demonstrated in the Python example. This is critical for security. Expected outcome: Reduced attack surface.
  • Consider cold start latency for headless browsers (Difficulty: Medium, Time: 2-4 hours): If self-hosting headless Chromium, factor in the ~7.8 s cold start penalty. For user-facing synchronous requests, pre-warming or an API is essential. Expected outcome: Realistic performance expectations and architectural choices.
  • Offload PDF generation to background processes (Difficulty: High, Time: 8-16 hours): If integrating headless browsers directly into a synchronous web framework like Django, move the rendering logic to a separate thread, process, or background queue to avoid issues like SynchronousOnlyOperation. Expected outcome: Stable application behavior and improved responsiveness.
  • Choose wisely based on volume (Difficulty: Low, Time: 0.5 hours): For applications generating fewer than 50 documents per month, a local library might be sufficient and cost-effective. For higher volumes, or if you want to avoid infrastructure maintenance, a hosted API like PDFGeny offers predictable performance (median render time of 0.6 s) and features like batch processing (up to 100 documents in one call) and PDF/A-2b output. Expected outcome: Optimized cost and maintenance.

Ready to move past maintenance headaches? PDFGeny handles the complexities of PDF generation, offering headless Chromium, WeasyPrint, and Ghostscript for PDF/A-2b. Send HTML, a URL, or choose from 40 templates, and get your PDF back in a median of 0.6 seconds. Focus on your application, not your PDF renderer.

Get a free API key

FAQ Section

Is wkhtmltopdf still maintained?

No, wkhtmltopdf is not actively maintained. Its last stable release (0.12.6) was in 2019, and it relies on an older WebKit engine that does not support modern CSS features, leading to rendering discrepancies compared to current browsers.

What are the typical performance issues with self-hosting headless Chromium for PDF generation?

The primary performance issue is cold start latency. A fresh headless Chromium instance can take approximately 7.8 seconds to initialize. Keeping instances warm reduces this to around 0.65 seconds, but requires continuous resource consumption and careful management to avoid memory leaks.

How can I prevent Server-Side Request Forgery (SSRF) when converting URLs to PDFs?

To prevent SSRF, you must validate all incoming URLs. Resolve the hostname to an IP address and reject private IP ranges (e.g., 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), loopback addresses (127.0.0.0/8), and cloud metadata service IPs (e.g., 169.254.169.254/16). This ensures the renderer cannot access internal network resources.

When is a PDF generation API like PDFGeny a better choice than a local library?

An API like PDFGeny is generally a better choice when you need to avoid the operational overhead of maintaining rendering infrastructure, handle high volumes of documents, or require specific features like PDF/A-2b output. PDFGeny offers a median render time of 0.6 seconds, supports batch processing of up to 100 documents, and provides a free plan for 50 documents a month, making it suitable for scaling without managing servers or handling cold starts. For low volumes (a few documents daily), a local library might be sufficient.

Generate PDF SEO Reports: A Deep Dive into Automation

M

Mikel Rougstone

Founder, PDFGeny

I build and run PDFGeny — the API, the rendering fleet and the template catalog. Most of what I write here comes from something that broke in production first.