October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsPC HealthRecommendedCrashes, freezes, slowdowns? Check your PC nowSpot repairable issues before they interrupt work.Check PCOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
browser automation

How to Fix Puppeteer PDF Race Conditions with Front-End Events

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

A Puppeteer PDF race occurs when page.pdf() runs before your application has finished the asynchronous work that the document depends on. The reliable fix is an application-owned readiness contract: reset a flag (or event) for each export, set it only after data, charts, layout, images, and other PDF content are complete, then have Puppeteer wait for that signal with a finite timeout before printing.

The readiness handshake that prevents the race

Puppeteer can wait for navigation milestones, network idleness, selectors, and page-side functions. None of those automatically knows what “finished” means for your application. A report may still be drawing a canvas, applying state, loading an image, or calculating layout after the network has gone quiet.

Use a flag owned by the page. The name window.__PDF_READY__ is only an application convention; it is not a Puppeteer built-in.

1. Initialize and reset readiness in the front end

<script>
  // Set this before starting work for the current export.
  window.__PDF_READY__ = false;

  async function renderReport() {
    try {
      const report = await fetch('/api/report').then(r => {
        if (!r.ok) throw new Error(`Report request failed: ${r.status}`);
        return r.json();
      });

      renderText(report);
      await renderCharts(report);       // canvas/SVG work
      await loadReportImages();         // wait for image decode
      await settleLayout();             // app-specific layout work

      // Set true only after every PDF-relevant operation has completed.
      window.__PDF_READY__ = true;
    } catch (error) {
      window.__PDF_ERROR__ = String(error);
      // Do not set the ready flag after a failed render.
    }
  }

  function settleLayout() {
    return new Promise(resolve => requestAnimationFrame(() =>
      requestAnimationFrame(resolve)
    ));
  }

  renderReport();
</script>

Reset the state for every export or job. If a page can render multiple reports without a full navigation, a boolean alone may be unsafe: an old true value could release a later PDF immediately. Use a job identifier or a one-shot promise so the signal is associated with the current document.

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

2. Wait for the signal, then print

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

try {
  await page.goto('https://example.com/report/42', {
    waitUntil: 'domcontentloaded',
  });

  await page.waitForFunction(
    () => window.__PDF_READY__ === true,
    { timeout: 15_000 }
  );

  const pdf = await page.pdf({
    printBackground: true,
  });
  await import('node:fs/promises').then(fs => fs.writeFile('report.pdf', pdf));
} finally {
  await browser.close();
}

The 15-second value is illustrative. Set it from the expected workload and your operational deadline. A finite timeout turns a missing signal into a diagnosable failure instead of an indefinitely hanging worker.

3. Report failures instead of hanging

try {
  await page.waitForFunction(
    () => window.__PDF_READY__ === true,
    { timeout: 15_000 }
  );
} catch (error) {
  const state = await page.evaluate(() => ({
    ready: window.__PDF_READY__,
    renderError: window.__PDF_ERROR__ ?? null,
    title: document.title,
  }));
  throw new Error(`PDF render was not ready: ${JSON.stringify(state)}`, {
    cause: error,
  });
}

If rendering can fail, expose an application-specific error state and log it. Silently changing the flag to true in a finally block produces a PDF that looks successful but is incomplete.

Events instead of a global flag

An event is useful when your rendering pipeline already emits lifecycle notifications. The event must be installed before the work begins and consumed once for the current job.

Page-side promise with a job id

// In the page application
window.__PDF_JOB__ = crypto.randomUUID();
window.__PDF_READY_PROMISE__ = new Promise(resolve => {
  window.__resolvePdfReady = resolve;
});

async function render() {
  // ...load data and finish all PDF-relevant rendering...
  window.__resolvePdfReady({ job: window.__PDF_JOB__ });
}
render();
// In Node, wait for a condition that includes the current job
const expectedJob = await page.evaluate(() => window.__PDF_JOB__);
await page.waitForFunction(
  job => window.__PDF_READY_STATE__?.job === job,
  { timeout: 15_000 },
  expectedJob
);
await page.pdf({ printBackground: true });

The exact event wiring is application code. Another option is page.exposeFunction(), which installs a function on window that invokes a Node callback and resolves its promise. Whichever mechanism you choose, make the handshake one-shot and bind it to the current document or job.

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

Choose the right wait for each stage

Strategy What it establishes What it cannot establish Best use
Navigation lifecycle (domcontentloaded, load) A browser navigation milestone occurred. Arbitrary client-side rendering is complete. Initial document readiness.
Network idle Requests met the configured idle condition. Timers, local computation, canvas work, or state updates are finished. A useful network milestone on pages where request quiet is meaningful.
Selector or DOM condition A specified element or state exists. The element necessarily contains all final content. A stable, genuinely print-ready marker.
App-owned flag or event The application says its print-relevant work is complete. A correct handshake must be implemented and reset by you. Dynamic reports, charts, client-side data, and multi-step rendering.
Fixed delay A chosen amount of time elapsed. Whether rendering actually finished. Temporary diagnosis only, not correctness.

Use network idle as a milestone, not a verdict

await page.goto(url, { waitUntil: 'networkidle2' });
await page.waitForFunction(
  () => window.__PDF_READY__ === true,
  { timeout: 15_000 }
);
await page.pdf({ printBackground: true });

networkidle2 can reduce variability when requests are the main source of delay, but network idleness does not encode your application’s semantic completion. Keep the readiness wait even when navigation uses a network-idle option.

Avoid navigation ordering races

If a click triggers navigation, register the navigation wait before clicking. Starting the waits concurrently prevents a fast navigation from completing before the listener is attached.

const [response] = await Promise.all([
  page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
  page.click('[data-export-report]'),
]);

await page.waitForFunction(
  () => window.__PDF_READY__ === true,
  { timeout: 15_000 }
);
await page.pdf({ printBackground: true });

After navigation resolves, the application-ready wait remains a separate step. Navigation completion and report rendering completion are different facts.

Make print output match the intended document

Fonts

Puppeteer’s PDF generation waits for document.fonts.ready by default through the waitForFonts option. Do not add an arbitrary font sleep first. If font waiting stalls for a page rendered in the background, check whether bringing that page to the foreground is required by your browser setup.

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.

Media type

page.pdf() uses print CSS media by default. If the design is specifically authored for screen media, select it before printing:

await page.emulateMediaType('screen');
await page.pdf({ printBackground: true });

For print colors that must remain exact, use the documented CSS property in the page stylesheet:

* {
  -webkit-print-color-adjust: exact;
}

Images, charts, and layout

  • Wait for image loading and decoding, not merely the presence of an <img> element.
  • Resolve chart-library promises or animation-complete callbacks before signaling readiness.
  • Disable or finish animations that can capture an intermediate frame.
  • Include client-side pagination, totals, and conditional sections in the readiness contract.
  • Use a selector wait only when the selector represents the final state, not just a loading container.

End-to-end export function

export async function createReportPdf(url, outputPath) {
  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  page.setDefaultTimeout(15_000);

  try {
    await page.goto(url, { waitUntil: 'domcontentloaded' });
    await page.waitForFunction(
      () => window.__PDF_READY__ === true,
      { timeout: 15_000 }
    );
    await page.pdf({
      path: outputPath,
      printBackground: true,
      waitForFonts: true,
    });
  } catch (error) {
    const diagnostics = await page.evaluate(() => ({
      ready: window.__PDF_READY__ ?? null,
      renderError: window.__PDF_ERROR__ ?? null,
      url: location.href,
    })).catch(() => null);
    throw new Error(`PDF export failed: ${JSON.stringify(diagnostics)}`, {
      cause: error,
    });
  } finally {
    await browser.close();
  }
}

Verify option behavior against the Puppeteer version installed in your project. The official API pages examined for this guidance displayed version 25.12.0 on September 29, 2026; projects on another version should check their matching documentation.

Troubleshooting checklist

Timeout waiting for readiness

  • Cause: The flag was never initialized, was reset after the wait began, or a render promise rejected.
  • Fix: Inspect the page’s ready and error values, add a visible error state, and ensure every success path sets readiness exactly once.

The PDF contains an old report

  • Cause: A stale true value or event from a previous job released the next export.
  • Fix: Reset before each job and include a unique job id in the condition.

Network idle arrives but charts are blank

  • Cause: Canvas drawing or local computation continued after requests stopped.
  • Fix: Resolve the chart renderer and set the app-owned signal afterward.

Click occasionally misses navigation

  • Cause: waitForNavigation() was attached after click().
  • Fix: Use the documented Promise.all() pattern with both operations started together.

Colors or responsive layout are wrong

  • Cause: PDF generation selected print media, while the design expects screen media, or print color adjustment is not enabled.
  • Fix: Call emulateMediaType('screen') when appropriate and review print-specific CSS.

Fonts never settle

  • Cause: Font loading is blocked, or a background-page limitation affects font waiting.
  • Fix: Check network and font responses, keep waitForFonts enabled unless diagnosed otherwise, and consider page.bringToFront() for the affected page.

A fixed sleep appears to fix it

  • Cause: The delay happens to exceed the usual render time.
  • Fix: Replace it with a condition-based readiness contract; a slow run can outlast the sleep, while a fast run pays the unnecessary delay.

Performance and reliability practices

  • Signal after the last PDF-relevant operation, but not after unrelated analytics or background polling.
  • Keep the timeout finite and record elapsed time, URL, job id, and the page’s error state.
  • Prefer deterministic animation settings and stable data snapshots for repeatable exports.
  • Use network-idle navigation only where it helps; long-lived connections can make that milestone unsuitable.
  • Run a small set of representative reports in CI, including slow data, missing images, chart-heavy pages, and an explicit render failure.
  • Do not treat a successful HTTP response as proof that the PDF content is complete.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

For teams that do not want to operate Puppeteer infrastructure, ScreenshotNeo provides a website screenshot API and MCP server. It is not a replacement for an application-specific PDF readiness handshake when your report must coordinate private data and custom rendering, but it can handle ordinary URL capture with one request.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo documentation for options and response details. Cookie and consent banners are accepted before capture, and more than 60 known consent platforms, newsletter popups, and chat widgets are removed; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and whether it was billed. ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.

Python

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

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}`);
const bytes = await res.arrayBuffer();
await Bun.write('shot.webp', bytes);

The service supports PNG, JPEG, WebP, and PDF output, full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets and custom viewports, retina scale, custom CSS and JavaScript, clicks, selector/delay/network-idle waits, request blocking, headers, cookies, user agents, authorization, timezone and geolocation, transparent backgrounds, resizing, configurable-TTL caching, signed links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameters used by other screenshot APIs also work to ease migration.

Plan Price Included shots
Free $0 1,000/month; no card
Starter $5 3,000
Growth $15 15,000
Pro $39 60,000
Scale $99 250,000
Business $249 1,000,000

Every feature is included on every plan, and yearly billing gives two months free. Start with 1,000 free screenshots a month—no card required.

Frequently Asked Questions

Is window.__PDF_READY__ a Puppeteer feature?

No. It is an application-defined flag used as the condition for page.waitForFunction(); you can choose another name or an event protocol.

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.

Should I use networkidle0 or networkidle2?

Choose the navigation milestone that suits the page’s request pattern, then still wait for the app-owned readiness signal. Neither network-idle setting represents arbitrary local rendering work.

Can I generate a PDF without a readiness signal?

Only when the page is genuinely static or a reliable DOM condition fully represents completion. Dynamic reports are safer with an explicit signal.

What happens when the readiness timeout expires?

Puppeteer throws a timeout error. Capture the page URL, ready value, application error, and job id so the missing or failed render can be corrected.

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.

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

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
PC Slower Than It Used to Be?Free scan - under a minute
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.