Recommended Free Tools
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.
#1 Best Overall
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.
Rank #2
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.
Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minutePC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Choose 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.
Media type
page.pdf() uses print CSS media by default. If the design is specifically authored for screen media, select it before printing:
Rank #4
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
truevalue 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 afterclick(). - 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
waitForFontsenabled unless diagnosed otherwise, and considerpage.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.
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.
Best Value
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.
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.
Quick Recap
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.
Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errors




