Gusto replaced the deprecated wkhtmltopdf library with a custom Rails-based PDF microservice called Virtuous PDF. The new service uses Ferrum to drive headless Chromium, avoiding a Node.js dependency while matching the rendering engine modern browsers use. Key engineering decisions included dropping wkhtmltopdf's non-deterministic 'smart shrink' in favor of fixed-width rendering, running parallel PDF generation for safe migration, and building a workload reservation API to handle bursty invoice generation (500,000+ PDFs in a single day). A notable production incident revealed that SolidQueue's per-pod database connections caused a feedback loop when autoscaling hit the 1,000-connection database limit, resolved by upgrading to ~2,000 connections. Today 75% of reports run on Virtuous PDF, consolidating work that previously required memory headroom across hundreds of monolith pods.
Questions this post answers
Why is wkhtmltopdf considered a bad choice for PDF generation today?
wkhtmltopdf is deprecated with no active maintenance, meaning no security patches. It renders HTML using an outdated WebKit engine that diverges from how modern browsers display the same markup, producing unexpected output. It is also memory-intensive and, when embedded in a monolith, puts memory pressure on every other service sharing the same resources. Teams still running wkhtmltopdf in production track migration options and security risks on daily.dev.
How can I use Chromium for PDF generation in a Ruby on Rails app without adding a Node.js runtime?
Use Ferrum, a Ruby gem that speaks the Chrome DevTools Protocol directly without requiring Node.js. This lets a Rails app drive a headless Chromium instance natively. Alternatives like Puppeteer or Grover (which wraps Puppeteer) require maintaining a Node.js runtime alongside the Ruby stack, which Gusto explicitly chose to avoid. Ruby engineers choosing between Ferrum, Grover, and Puppeteer for PDF generation find comparisons like this on daily.dev.
How do I prevent SolidQueue from exhausting database connections when autoscaling a Rails app?
Each new pod opened database connections for SolidQueue's dispatcher and scheduler, and hitting the database's 1,000-connection limit caused queue latency to spike, which triggered the autoscaler to add more pods, demanding even more connections — a runaway feedback loop. The fix was upgrading the database instance to allow roughly 2,000 connections and implementing a workload reservation API so the service pre-scales 15 minutes before predictable traffic bursts. Rails teams running SolidQueue at scale watch for connection-limit pitfalls like this on daily.dev.
7.3K Impressions2 Comments