Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If Puppeteer does not create a PDF, first prove that Chromium launched, the page finished its own loading work, and the process can write the destination. Use an absolute path while diagnosing, wait for application data and fonts, then call page.pdf() before closing the browser. An undefined path returns PDF bytes instead of creating a file.

Start with a known-good PDF script

The smallest reliable sequence is: launch Chromium, create a page, navigate, wait for the page to be ready, print with page.pdf(), and close the browser. The Puppeteer PDF guide’s canonical instruction is: “For printing PDFs use Page.pdf().”

import puppeteer from 'puppeteer';

const url = 'https://example.com';
const output = '/tmp/output.pdf';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto(url, { waitUntil: 'networkidle2' });

  // Replace this with your app's real readiness condition when needed.
  await page.evaluate(() => document.fonts.ready);

  await page.pdf({
    path: output,
    format: 'A4',
    printBackground: true
  });

  console.log(`Wrote ${output}`);
} finally {
  await browser.close();
}

Run this from a directory where Puppeteer and its compatible browser are installed. During diagnosis, keep /tmp/output.pdf as an absolute destination. Once it works, you can change the path to a directory your service owns.

Confirm what “does not generate a PDF” means

The call returns bytes but no file appears

Puppeteer only writes a disk file when the PDF options include path. If path is omitted or undefined, page.pdf() returns a buffer and does not create a file. Relative paths are resolved from the process’s current working directory, which may differ between a shell, a test runner, a container, and a service manager.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const pdf = await page.pdf({ format: 'A4' });
await fs.promises.writeFile('/tmp/output.pdf', pdf);

For a direct file write, use an explicit absolute path:

await page.pdf({ path: '/tmp/output.pdf', format: 'A4' });

The file is written somewhere unexpected

Log both process.cwd() and the resolved destination, then inspect the parent directory’s permissions. A path such as reports/invoice.pdf is relative to the current working directory, not necessarily the directory containing your JavaScript file.

import path from 'node:path';
console.log({ cwd: process.cwd(), destination: path.resolve('reports/invoice.pdf') });

The file exists but is zero bytes or cannot be opened

Do not close the browser until the page.pdf() promise resolves. Check that the parent directory exists and is writable, and verify the process is not terminating early because an earlier navigation or selector promise rejected.

Make navigation and application readiness explicit

Choose a navigation condition that matches the site

waitUntil: 'networkidle2' is useful for pages that become quiet after loading, but it is not a substitute for an application-specific readiness check. Dashboards, streaming pages, analytics connections, and long polls may continue network activity even after the content you need is visible.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForSelector('[data-report-ready]', { timeout: 30000 });

If the page signals readiness through an API response, wait for that response or for a DOM marker your application sets after rendering. Increase a timeout only after identifying the operation that is legitimately slow.

Wait for fonts and late data

PDF generation waits for fonts by default. A web font that has not loaded, or data that is still being inserted when printing starts, can make output look different, incomplete, or appear to hang. Keep the default font wait when you need the final font, and explicitly await your application’s data readiness. You can also make the font state visible during debugging:

await page.waitForSelector('#invoice-total');
await page.evaluate(() => document.fonts.ready);
await page.pdf({ path: '/tmp/invoice.pdf', format: 'A4' });

Detect a failed page instead of printing an error screen

Check the HTTP response and capture browser console or page errors. A server-side error page can still produce a valid PDF, so navigation success alone does not prove that the intended document rendered.

page.on('console', message => console.log('[browser]', message.text()));
page.on('pageerror', error => console.error('[page error]', error));
const response = await page.goto(url, { waitUntil: 'networkidle2' });
if (!response || !response.ok()) {
  throw new Error(`Navigation failed: ${response?.status() ?? 'no response'}`);
}

Separate Chromium launch failures from PDF failures

Run a minimal launch before debugging HTML, CSS, or PDF options. If this fails, page.pdf() is not the problem.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
console.log(await browser.version());
await browser.close();

Missing Linux libraries or fonts

Minimal Debian or Ubuntu images often lack libraries required by Chrome for Testing. Puppeteer’s documented dependency set includes libatk-bridge2.0-0, libatk1.0-0, libcairo2, libgbm1, libnss3, libpango-1.0-0, libpangocairo-1.0-0, and suitable font packages. Install the packages in the image used at runtime, not just on your laptop.

When Chrome reports a shared-library error, inspect the executable with:

ldd /path/to/chrome | grep not

Any reported library marked “not found” must be installed or supplied by the runtime image. Missing fonts can also produce blank-looking text or unexpected line wrapping even when Chromium launches.

Sandbox and security policy errors

No usable sandbox! indicates an environment problem, usually user-namespace restrictions or an unsuitable container configuration. Configure a supported sandbox first. Puppeteer documents --no-sandbox only for trusted content and strongly discourages disabling the sandbox; it is not a general fix for production deployments.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

On Ubuntu, AppArmor profiles can prevent Chrome for Testing from using user namespaces. Review the host security policy and container privileges instead of immediately adding unsafe flags.

Profile and cache directories are not writable

Chrome writes profile, configuration, and cache data. Read-only containers commonly fail with permission errors after a successful local run. Point runtime directories and the browser profile at writable storage:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({
  userDataDir: '/tmp/.puppeteer-profile',
  env: {
    ...process.env,
    XDG_CONFIG_HOME: '/tmp/.chromium/config',
    XDG_CACHE_HOME: '/tmp/.chromium/cache'
  }
});

Create those directories during startup if your base image does not create them, and make sure the service account can write to them.

Control print media and page layout

Screen CSS versus print CSS

PDF output uses print media by default. If the design you need is the screen version, set the media type before printing:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.emulateMediaType('screen');
await page.pdf({
  path: '/tmp/screen-styled.pdf',
  format: 'A4',
  printBackground: true
});

Backgrounds, CSS page size, and margins

  • printBackground: true preserves background colors and images that print styles otherwise omit.
  • preferCSSPageSize: true lets the document’s @page rule take priority over the format option.
  • Explicit margins and paper settings prevent content from being clipped when a template assumes a particular page size.
await page.pdf({
  path: '/tmp/report.pdf',
  printBackground: true,
  preferCSSPageSize: true,
  margin: { top: '16mm', right: '14mm', bottom: '16mm', left: '14mm' }
});

If a document is unexpectedly paginated, inspect its @page rule, fixed-height containers, overflow settings, and print-only styles before changing browser launch flags.

Cloud Run, Lambda, and container deployment checks

Google Cloud Run

Cloud Run’s default Node runtime does not include all system packages needed by Headless Chrome, so use an image that installs them. Also perform PDF work before sending the HTTP response or enable CPU always: after a response, CPU is disabled by default, and starting Puppeteer afterward can become very slow.

Use writable locations such as /tmp for the PDF and Chromium profile. Keep the browser launch, navigation, and print inside the request’s active CPU window, and close the browser in a finally block.

AWS Lambda

Lambda deployment-package limits make bundling a full browser difficult. Use a Chromium packaging strategy compatible with your runtime, verify the executable path, and write temporary files under Lambda’s writable temporary directory. A local executable path that works on macOS or a development container will not automatically exist in Lambda.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • Used Book in Good Condition

Any read-only or restricted container

Validate these items in the deployed image itself:

  • Chromium launches under the service user.
  • Required shared libraries and fonts are installed.
  • The sandbox policy permits the chosen browser configuration.
  • /tmp or another configured directory is writable.
  • The browser profile and cache do not point to a read-only home directory.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

A diagnostic workflow that avoids guesswork

  1. Prove launch: run puppeteer.launch(), create a page, print the browser version, and close it.
  2. Prove writing: use an absolute path such as /tmp/output.pdf and test parent-directory permissions.
  3. Prove navigation: log the response status and listen for page errors.
  4. Prove readiness: wait for the selector, API result, or application flag that means the document is complete.
  5. Prove fonts: await document.fonts.ready when typography matters.
  6. Prove print settings: choose screen or print media, then set background and CSS-page-size behavior deliberately.
  7. Prove deployment: repeat the same checks in Cloud Run, Lambda, or the production container rather than relying on local success.

Common symptoms and precise fixes

Symptom Likely cause Fix
No file, but no exception path is missing or relative to an unexpected working directory. Set an absolute path, or write the returned buffer yourself.
ENOENT or permission denied when saving The parent directory does not exist or is not writable. Create it or use a writable absolute destination such as /tmp/output.pdf.
PDF promise never finishes Navigation, fonts, or application data never reaches readiness. Use the right waitUntil, wait for a specific selector or response, and inspect page errors.
Blank or nearly blank PDF Content is rendered after printing, a failed page was printed, or required fonts/resources are unavailable. Wait for the rendered marker, check the response and console, and verify fonts and dependencies.
Failed to launch the browser process Missing libraries, an invalid executable path, or an incompatible runtime image. Run the minimal launch test, install dependencies, and inspect ldd output.
No usable sandbox! Container or host security policy blocks the sandbox. Configure a supported sandbox and review AppArmor/user-namespace policy; avoid disabling the sandbox for untrusted content.
EACCES for profile or cache Chrome’s home, profile, or cache directory is read-only. Set writable XDG directories and userDataDir under /tmp.
Screen colors or layout are missing Print media rules are active or backgrounds are disabled. Call emulateMediaType('screen') and set printBackground: true.
Works locally, slow on Cloud Run CPU is unavailable after the response or the image lacks Chrome libraries. Finish the job before responding or enable CPU always, and use an image with the required packages.
Works locally, fails on Lambda The browser binary is too large, missing, or located elsewhere. Use a Lambda-compatible Chromium package and verify the runtime executable path.

Performance and reliability practices

Keep the critical path deterministic

Reuse one browser process for a batch when the runtime allows it, but create a fresh page per document and close each page in a finally block. Avoid arbitrary sleeps as the primary readiness mechanism; a selector, response, or application flag is both faster and more reliable.

Bound every external wait

Set navigation and selector timeouts that match your service’s request budget. Record which stage timed out—launch, navigation, readiness, fonts, or PDF writing—so retries address the failing stage instead of repeating an unknown failure.

Make output and retries safe

Write to a temporary filename, verify that the PDF promise completed, then move or rename the file to its final name. For retries, use a unique destination or clean up the previous temporary file so a failed attempt cannot be mistaken for a fresh document.

Or skip the browser setup

If your requirement is simply to capture a URL as an image or PDF, ScreenshotNeo provides a hosted endpoint and an MCP server instead of making you package Chromium. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be disabled. Only clean shots are billed: bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the result with X-Page-Verdict and X-Billed headers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The API base is https://api.screenshotneo.com/v1/shot. This one-call example follows the documented request format:

curl -G 'https://api.screenshotneo.com/v1/shot' -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

For Python:

import requests
r = requests.get('https://api.screenshotneo.com/v1/shot', params={'access_key': 'YOUR_API_KEY', 'url': 'https://stripe.com'}, timeout=90)
r.raise_for_status()
open('shot.webp', 'wb').write(r.content)

For Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`ScreenshotNeo failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

See the ScreenshotNeo API documentation for PDF output and the other capture controls. Its MCP tools—take_screenshot, get_page_info, and capture_pdf—let Claude, Cursor, or another MCP client perform captures. Every plan includes the features; 1,000 screenshots per month are free with no card, and paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account to try it.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.