Skip to content

Generating PDFs your customers can actually open

  • Home
  • Blog
  • Generating PDFs your customers can actually open
Generating PDFs your customers can actually open

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.
How a PDF request travels from customer to downloadOrdered stages from customer request through background rendering to delivery, connected by arrows.How a PDF request should travel1CustomerrequestRecord ID, notraw HTML2Queue joboff theweb threadSurvives longrender times3Render withfontsembeddedDevanagari, ₹,diacritics4Verifypages andtextSelectable,searchable5Store anddeliverSigned URL,correct MIME
The path a customer-facing PDF should take: request and queue first, render with embedded fonts, verify the output, then deliver with correct headers.

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.

EngineFidelityRuns JavaScriptServer loadBest when
Playwright / PuppeteerHigh — real ChromiumYesHigh per jobComplex HTML, charts, client-side templates
WeasyPrintGood for HTML + CSSNoLowServer-rendered invoices and statements
Dompdf / TCPDFModerate, manual layoutNoLowExisting Laravel apps with simple grids
jsPDF + html2canvasRaster, not selectablen/aNone on serverOne-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:

  1. Find every route that currently streams a PDF and list its data source.
  2. Add a dedicated queue — Laravel queues, BullMQ or Sidekiq — with a pdf queue name.
  3. Move the render call into a job class and dispatch it with the record ID, never with raw user-supplied HTML.
  4. Write the finished file to object storage under a generated key, outside the web root.
  5. Return a signed URL or a poll endpoint to the browser; the request itself stays fast.
  6. 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.

  1. Open the file in at least three viewers: a desktop browser, Adobe Reader and a mobile viewer.
  2. Select and copy a paragraph, then paste it into a text editor and compare it character by character.
  3. Search for a name with Devanagari or diacritics and for a currency amount; both must match exactly.
  4. Check the page count and look for rows split across page breaks or headers repeated incorrectly.
  5. Confirm the file size is sane — not zero bytes, not unexpectedly large for the content.
  6. 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.

Lifecycle of a generated PDF from data to deliveryTimeline of milestones for generating and verifying a customer-facing PDF, with a checklist band.Lifecycle of a generated PDF1Data andtemplate ready2Job queuedoff the web thread3Rendered withfonts embedded4Pages and textverified5Stored anddeliveredCheck before shippingOpen the file in a browser, Adobe Reader and a phone. Select the text. Search for a real customer name.If any of those fail, the job is not done.
The lifecycle of a customer-facing document: queue it, render with embedded fonts, verify the output across viewers, then store and deliver.

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.

Which document approach fits which customer needRows mapping each document approach to the customer need it suits best.Which document approach appliesHTML + CSSScreen-first statements and previews where exact pagination does not matterWeasyPrintServer-rendered invoices and letters with predictable memory and no JavaScriptChromiumJavaScript charts and pixel-faithful reports where fidelity beats memory costjsPDFOne-off client-only downloads where selectable text is not required
How the common document approaches map to pagination needs, JavaScript use, memory limits and whether text must stay selectable.

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/pdf and 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

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.

Frequently asked questions

  • Usually the generator emitted a malformed PDF or the HTTP response has wrong MIME type or buffering truncated the file. Check Content-Type: application/pdf and Content-Length, then open the file offline. If it opens locally, the fault is response streaming or a proxy interrupting the download.

  • For HTML-to-PDF, headless Chromium via Puppeteer or Playwright is dependable because it uses the same rendering engine as Chrome. For lower-level layout, PDF libraries like ReportLab or TCPDF work but require manual positioning. Choose the headless browser for CSS fidelity; pin the exact browser version in CI.

  • Use server-side generation when the PDF must look identical across devices, include data the user cannot see, or be emailed and archived. Browser print-to-PDF depends on the visitor's OS, fonts and printer settings. Server generation gives you one deterministic output and lets you queue large jobs.

  • Parse the output with a PDF reader library such as pdfinfo, qpdf --check or PyPDF2 and confirm the page count, fonts and no syntax errors. Then open the file in Chrome, Firefox and a desktop PDF viewer. Automate this as a smoke test after each deploy because a wrong font or truncated stream can still pass basic checks.

  • The message appears when Chrome cannot parse the file, often because the PDF is truncated, starts with a UTF-8 BOM, or the server sends Content-Type: text/html. Download the response with curl -v, check the first bytes for %PDF, and compare the byte length with the Content-Length header.

  • Headless Chromium can consume hundreds of megabytes per render and time out on multi-hundred-page reports. If generation fails intermittently, watch the process RSS and CPU; increase the job timeout, run a worker queue, and split large documents into chapters. For repeated failures, capture the Chromium stderr to see the actual crash reason.

  • Embed a font that covers the required scripts. Headless Chromium does this automatically when a system font is available; otherwise install fonts like Noto or a specific CJK/Devanagari font on the server. Test with sample text and check the PDF's font list with pdffonts to confirm the glyphs are not shown as boxes.

  • Treat it like rendering untrusted web content. A headless browser can load external resources, exfiltrate local network data, or run JavaScript. Disable JavaScript where possible, block network requests to unknown origins, set a strict navigation allowlist, and run the renderer in an isolated container with no access to internal services.

  • Browser print-to-PDF relies on the user's installed fonts, default margins and print dialogue; output varies by OS. Server-side generation applies your exact CSS @page rules, headers and footers every time. For invoices or contracts sent to customers, server-side gives repeatable branding and lets you store the exact file you emailed.

  • The main drivers are CPU and memory for each render, storage for stored PDFs, and egress when customers download them. Headless Chromium jobs are heavier than a PDF library. Run generation in a queue with concurrency limits, cache repeat outputs, and monitor render duration and failure rate. Vendor prices change; use their calculator or contact us.

0 comments

Be the first to share your thoughts.

Leave a comment

Chat on WhatsApp