A PDF that renders on your laptop but opens as blank boxes, times out mid-download, or is rejected by the customer's viewer is almost always a font, queueing, or content-length problem. In production, pdf generation in a web application belongs in a background worker with embedded fonts, verified output, and correct download headers.
Key Takeaways
- Most "the file won't open" failures come from unembedded fonts, truncated responses, or a wrong MIME type — not from the PDF library itself.
- Generate PDFs in a queue worker, never in the HTTP request thread, where a 60-second nginx proxy timeout can cut the file mid-write.
- Embed a font that covers every range you accept, including Devanagari names and the rupee sign ₹; a missing glyph renders as boxes.
- Verify output in a browser, Adobe Reader and a phone, then confirm the text is selectable and searchable before the first customer sees it.
- Headless Chromium gives the most faithful HTML rendering; WeasyPrint is lighter but runs no JavaScript — choose by fidelity, memory and what your team already operates.
- When customers only view or print on demand, a styled HTML page with print CSS is often the cheaper, safer option than a generated PDF.
What actually makes a PDF fail to open for a customer?
Most "the file won't open" complaints trace to three faults. The font was not embedded, so text renders as boxes or substitutes. The server cut the response short, leaving a truncated file. Or the wrong MIME type made the browser store a stream it could not interpret. Each fault has a distinct fix, so identify it before changing code.
A valid PDF starts with the header %PDF- and ends with an end-of-file marker. When a worker runs out of memory mid-render or an nginx proxy read timeout fires, the file keeps the header but loses the tail. The customer's viewer opens a blank page or reports a corrupt file. Check the byte length against what the generator expected; a mismatch almost always means the response was cut.
Fonts are the subtler failure. A PDF stores references to the fonts used; if the font is not embedded, the viewer substitutes whatever it has installed. A Nepali name like सुमन श्रेष्ठ, a rupee amount, or an accented European surname can turn into squares on a customer's phone while looking perfect in your test environment. The viewer also matters: Adobe Reader, Chrome's built-in viewer and iOS Preview differ in how aggressively they substitute missing glyphs.
Which rendering engine should a web application use?
No engine wins everywhere. Headless Chromium through Playwright or Puppeteer renders real CSS and JavaScript, at the cost of significant memory per job. WeasyPrint is lighter but ignores JavaScript. PHP libraries such as Dompdf live inside Laravel but need manual layout for anything beyond simple grids. Choose by fidelity, peak memory, and what your team already operates.
| Engine | Fidelity | Runs JavaScript | Server load | Best when |
|---|---|---|---|---|
| Playwright / Puppeteer | High — real Chromium | Yes | High per job | Complex HTML, charts, client-side templates |
| WeasyPrint | Good for HTML + CSS | No | Low | Server-rendered invoices and statements |
| Dompdf / TCPDF | Moderate, manual layout | No | Low | Existing Laravel apps with simple grids |
| jsPDF + html2canvas | Raster, not selectable | n/a | None on server | One-off client-only downloads |
WeasyPrint is a strong default for a web application that builds server-side HTML templates and does not depend on client-side rendering. It handles print CSS well, embeds fonts reliably, and keeps memory predictable. If your documents use charts rendered in JavaScript, headless Chromium is the more faithful choice — budget for the heavier worker. wkhtmltopdf still appears in older systems but is effectively unmaintained; treat it as legacy and plan a move.
How do you move PDF generation out of the HTTP request?
Generate in a queue worker, not the request thread. A synchronous render competes with the proxy read timeout — commonly 60 seconds on nginx — and a slow report returns a truncated file the customer cannot open. Queue the job, render in a worker, store the result, and hand back a download link instead of streaming inline.
This also changes your failure mode for the better. A crashed render becomes a failed job you can retry with backoff, not a broken customer request. Here is the sequence we recommend:
- Find every route that currently streams a PDF and list its data source.
- Add a dedicated queue — Laravel queues, BullMQ or Sidekiq — with a
pdfqueue name. - Move the render call into a job class and dispatch it with the record ID, never with raw user-supplied HTML.
- Write the finished file to object storage under a generated key, outside the web root.
- Return a signed URL or a poll endpoint to the browser; the request itself stays fast.
- Alert on failed jobs and set retry limits so one bad template cannot loop forever.
// Laravel: dispatch the render, return immediately
GenerateInvoicePdf::dispatch($invoice)->onQueue('pdf');
// Worker: Chromium renders the page, then writes a real PDF
await page.pdf({ path: 'invoice-1042.pdf', format: 'A4', printBackground: true }); Size the worker to the engine. One Chromium render can consume a large share of a container's memory, so run one render per worker process and cap concurrency. A memory limit hit mid-job produces exactly the truncated file we warned about earlier.
What fonts and Unicode ranges do you have to handle?
Any name, address or amount outside Latin-1 can render as boxes when the font is not embedded. Nepali text needs Devanagari coverage; invoices need the rupee sign ₹; older viewers may substitute the entire face. Bundle a font that covers every range you accept and embed it, subsetting if file size matters.
Take a typical case: a travel agency in Kathmandu sends booking confirmations with guest names in Devanagari and amounts in rupees. The first build uses the server's default font, which lacks those glyphs. The PDF looks fine in the developer's browser because the browser falls back to a system font, but the customer's PDF viewer does not. The fix is explicit: install a font such as Noto Sans Devanagari on the renderer and reference it in the CSS, so the glyphs are embedded in the file itself.
This is the same class of problem that affects a customer portal producing statements or receipts. Test with real data — a long address, a name with a diacritic, a currency symbol — not with lorem ipsum. If you cannot paste the text back out of the PDF and read it, the font is wrong.
How do you verify a generated PDF before customers see it?
Open the file in a browser, Adobe Reader and a phone. Select the text and search for a known customer name. If text is not selectable, you shipped a raster image; if search misses, the encoding is wrong. Run these checks against real production-shaped data, not sample rows, before the first customer download.
- Open the file in at least three viewers: a desktop browser, Adobe Reader and a mobile viewer.
- Select and copy a paragraph, then paste it into a text editor and compare it character by character.
- Search for a name with Devanagari or diacritics and for a currency amount; both must match exactly.
- Check the page count and look for rows split across page breaks or headers repeated incorrectly.
- Confirm the file size is sane — not zero bytes, not unexpectedly large for the content.
- Read the document metadata and remove internal paths, template names or generator versions that leak your stack.
Verification is not a one-time task. Add a smoke check to the queue worker that opens each generated file programmatically and asserts the page count is above zero and the text layer contains an expected string. That catches regressions in templates, fonts and dependencies before a customer reports them.
What does it cost to run this well, and what breaks at scale?
Cost is driven by renderer memory per concurrent job, storage for generated files, and engineer time for edge cases like page breaks and fonts. One worker with a queue handles steady invoice volume cheaply. Bursty month-end runs need more workers or a longer queue, which trades latency for idle capacity you still pay for.
Storage is usually the smallest line item until you add retention and egress. If customers download the same statement repeatedly, every download counts as outbound traffic on most clouds. Use a sensible retention policy and let old files move to a cheaper storage class rather than sitting in hot storage forever. The recurring cost that surprises teams is not infrastructure — it is the maintenance of templates and fonts as business rules change.
Scaling pain usually appears at month-end, not in steady state. A queue that empties in minutes on a normal day can back up for hours when every customer requests a statement at once. Watch queue depth and worker memory together; a worker that is busy but swapping is slower than one that fails fast.
What security mistakes are common with generated PDFs?
PDFs leak more than teams expect: internal filesystem paths, template names, sometimes font metadata that identifies your stack. The bigger risks are rendering untrusted HTML without a sandbox, fetching remote images — an SSRF vector — and path traversal when a user can influence the output filename. Treat templates as code and inputs as hostile.
Keep the renderer sandboxed and never pass raw user HTML into the layout engine without stripping scripts and external resource references. If your documents include remote images, allowlist the hosts and render them server-side first. Generate output filenames from a server-side ID, never from user input, and store files outside the web root so a guessed URL cannot expose another customer's document.
Scrub metadata before delivery. The producer and author fields often contain library versions and internal usernames that make an attacker's reconnaissance easier. A generated document is a published artefact; assume it will be forwarded outside your organisation.
When is a printable HTML page the better choice?
If customers only view or print on demand, a styled HTML page with print CSS is cheaper and easier to keep correct than a generated PDF. Produce a real PDF when the file must be emailed, stored, signed or opened offline — not because a PDF looks official. The simpler option often wins for internal reports and draft previews.
Browsers already solve the font problem: they fall back to system fonts, so a Devanagari name renders without you embedding anything. The trade-off is that print output varies by browser and operating system, and you cannot guarantee exact pagination. For a formal invoice that must match a legal template, that variability is unacceptable. For a screen-first statement, it may be fine.
This decision matters because a generated PDF pipeline is permanent operational surface. It has a queue, a worker, a font set and a storage policy. If a printable HTML view meets the need, you avoid all of that. Our team can help you scope a custom web application and choose the right document path before you build it.
What do teams get wrong the first time?
The first version usually renders in the request thread, uses default fonts, and is tested only with English sample data. Then a real name with a diacritic or Devanagari breaks it on a customer's phone. We have seen all three fail in the same week. Fix the queue, the fonts and the verification before launch, not after the first support ticket.
A common mistake is treating PDFs as a view-layer concern. They are a background processing concern with their own failure modes, retries and resource limits. Another is changing engines mid-project and assuming the CSS behaves identically — page-break rules and print media queries differ between WeasyPrint and Chromium. Test the switch against the same golden files, not against a handful of manual checks.
The cheapest time to catch these problems is before the first customer download. Generate a batch of real records in staging, open them across viewers and devices, and have a non-engineer confirm the output looks right. If a name, amount or page break is wrong, you will hear about it immediately instead of through a refund request.
In short
- Put pdf generation in a background worker, never in the request thread.
- Embed a font that covers Devanagari, diacritics and the rupee sign.
- Deliver with
application/pdfand a proper filename header. - Verify files open in multiple viewers with selectable, searchable text.
- Choose the lightest engine that meets your fidelity needs, and treat templates as code.
People also search for
- What is the difference between a website and a web application?
- How much does a custom web application cost to build?
- What does web application maintenance actually cost?
- Is a customer portal worth building for statements and invoices?
- Should we build custom software or buy an off-the-shelf tool?
- What customer data should we actually collect?
- Shared hosting vs VPS vs cloud: which fits a growing app?
If your application now generates PDFs that intermittently fail, or you are planning a customer-facing document feature, our team can review your current setup, move the rendering into a queue, fix font embedding for Nepali and international text, and add the verification checks that catch failures before customers do. See the work we have shipped or tell us what you are building.












0 comments
Be the first to share your thoughts.
Leave a comment
Replying to — cancel