How to Wait for AJAX Before Printing to PDF With Headless Chrome
Wait for AJAX results before printing with Puppeteer: use network idle as a starting point, then wait for the application’s actual ready state.

To wait for AJAX before printing a page to PDF with headless Chrome, first navigate with Puppeteer’s waitUntil: 'networkidle2', then call page.pdf(). Treat network idle as a starting heuristic: if the page exposes a reliable ready marker, wait for that marker too. Network quiet alone does not prove that the application has processed a response and rendered the result you need.
Puppeteer’s official PDF guide uses that navigation-and-print sequence and recommends Page.pdf() for PDF output. It also notes that PDF generation waits for fonts by default. Puppeteer PDF generation guide.
1. Start with network idle
Install Puppeteer in a Node.js project, save the following as print-pdf.js, and run it with a page URL. Puppeteer provides the browser automation API and a compatible browser installation; follow its installation instructions for your environment.
const puppeteer = require('puppeteer');
async function main() {
const url = process.argv[2];
if (!url) {
throw new Error('Usage: node print-pdf.js <url>');
}
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
page.setDefaultNavigationTimeout(45_000);
await page.goto(url, { waitUntil: 'networkidle2' });
await page.pdf({ path: 'output.pdf', format: 'A4', printBackground: true });
console.log('Wrote output.pdf');
} finally {
await browser.close();
}
}
main().catch(error => {
console.error(error);
process.exitCode = 1;
});
Example: node print-pdf.js https://example.com/report. The finally block closes Chrome even if navigation or PDF generation throws. Set timeouts to fit the site and your job budget; there is no universal wait duration for every application.
networkidle2 is a navigation lifecycle condition based on network activity. It works as a convenient baseline when the page’s initial requests settle. If the site keeps polling, opens long-lived connections, or starts another request after navigation, waiting for network idle may be delayed or may happen before the application’s final visible state is ready. The API documents network activity, not the completion of every app callback and render.
2. Wait for the application’s ready state
If the page indicates that its report or results are ready, wait for that signal. A data attribute, a visible results container, a completed status, or disappearance of a loading element is usually more meaningful than a fixed sleep. Replace the example selector and predicate with a state the target application actually guarantees.

const puppeteer = require('puppeteer');
async function main() {
const url = process.argv[2];
if (!url) throw new Error('Usage: node print-ready.js <url>');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
page.setDefaultNavigationTimeout(45_000);
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => {
const results = document.querySelector('[data-report-ready="true"]');
return results && results.textContent.trim().length > 0;
}, { timeout: 30_000 });
await page.pdf({ path: 'output.pdf', format: 'A4', printBackground: true });
} finally {
await browser.close();
}
}
main().catch(error => {
console.error('PDF job failed:', error);
process.exitCode = 1;
});
waitForFunction() evaluates a page-context function until it returns a truthy value, and accepts options such as a timeout. If readiness means an element exists or becomes visible, waitForSelector() can express that more directly. See the official waitForFunction API and Page API.
Make the condition specific enough to rule out stale or empty content. For example, test for a completed status and at least one expected result row, or verify that a known report title has appeared. If the page can legitimately return zero results, use its explicit completed state rather than requiring non-empty text.
3. Choose the right wait for the page
| Situation | Wait strategy | What to watch |
|---|---|---|
| Initial navigation, requests settle | page.goto(url, { waitUntil: 'networkidle2' }) |
Useful baseline; confirm the content is actually ready. |
| Initial document parses, then app renders results | Navigate with domcontentloaded, then wait for a selector or predicate |
Use an app-owned state that represents completed data. |
| An in-page action triggers AJAX | Perform the action, then wait for its result marker or a network quiet period | Wait after the action; navigation lifecycle events may not recur. |
| No application signal exists | Use waitForNetworkIdle() with a bounded timeout, then inspect expected output |
Network idle remains a heuristic, especially with polling or background traffic. |
Puppeteer’s page.waitForNetworkIdle() waits for the network to be idle and waits at least the configured idle period. The documented default idleTime is 500 ms and concurrency is 0. These options let you tune a quiet-period wait, but they cannot tell whether the app considers its data complete. Refer to waitForNetworkIdle() and WaitForNetworkIdleOptions.

// Use after an action that triggers asynchronous requests.
await page.click('#refresh-report');
await page.waitForNetworkIdle({ idleTime: 800, concurrency: 0, timeout: 20_000 });
await page.waitForSelector('[data-report-state="complete"]', { timeout: 10_000 });
The selector after the quiet period provides an application-level check. If your app has a single dependable completion marker, that may be sufficient on its own. A fixed delay can serve as a deliberate final settling buffer after a readiness signal, but should not be the primary condition: elapsed time is not evidence that a particular request or render finished.
4. Handle an AJAX action before printing
For a page where a button or form starts the request, load the document, perform the action, and wait for the resulting state. If possible, wait for a changed marker or value so an old result cannot satisfy the condition before the new request completes.
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.click('#run-report');
await page.waitForFunction(() => {
const status = document.querySelector('#report-status');
return status?.getAttribute('data-state') === 'complete';
}, { timeout: 30_000 });
const rows = await page.$$eval('#report-results tr', els => els.length);
if (rows === 0) {
throw new Error('Report completed but contained no rows; check whether zero results are valid.');
}
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
This is a pattern, not a tested predicate for a particular site. Instrument the application’s actual loading and completion state. For pages under your control, expose a stable status in the DOM or a page-context value specifically intended for automation.
5. Configure the PDF output
page.pdf() generates output using the print CSS media type. If the PDF should match screen styling instead, call page.emulateMediaType('screen') before printing. Fonts are awaited by default, so a separate font wait is usually unnecessary. Check the PDF guide for supported output settings and examples.
await page.pdf({
path: 'report.pdf',
format: 'A4', // or use width and height
printBackground: true, // include CSS background colors and images
landscape: false,
margin: { top: '12mm', right: '12mm', bottom: '12mm', left: '12mm' },
preferCSSPageSize: true // honor CSS @page dimensions where appropriate
});
For screen media:
await page.emulateMediaType('screen');
await page.pdf({ path: 'screen-styled.pdf', printBackground: true });
Choose print or screen styling deliberately. A page can have separate print styles that hide navigation, adjust page breaks, or change colors. If a PDF is unexpectedly missing content, inspect both the page’s readiness condition and its print CSS.
6. Add bounded failure handling and useful diagnostics
Production jobs should fail visibly when the page never reaches its readiness condition. Set navigation and readiness timeouts, log the URL and failing stage, and capture diagnostic information on failure when your data-handling rules permit it. Avoid silently producing a success-shaped PDF after a timeout: it can look valid while omitting the AJAX result.
try {
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.waitForSelector('[data-report-state="complete"]', {
visible: true,
timeout: 30_000
});
await page.pdf({ path: 'report.pdf', format: 'A4', printBackground: true });
} catch (error) {
console.error(`Could not produce a ready PDF for ${url}:`, error.message);
// Record a failed job and retry only if the failure is transient.
throw error;
}
For repeated jobs, include a job identifier in logs and distinguish navigation timeout, readiness timeout, PDF error, and browser launch failure. Retry with a limit and only where retrying makes sense; a wrong selector or permanently inaccessible page will not be fixed by repeated attempts.
7. Common problems and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| PDF has a loading spinner or empty result area | The wait only covered navigation, not the AJAX completion and render. | Wait for an application-specific completion marker; verify expected content before printing. |
networkidle2 appears to hang |
Polling, analytics, streaming, or other ongoing traffic may keep activity above the threshold. | Use a ready selector/predicate. If network idle is still useful, bound its timeout and review page requests. |
| Network idle completes but the PDF is incomplete | Network inactivity occurred before app callbacks, state updates, or rendering finished. | Use a state tied to the required content; treat idle as a heuristic, not proof of readiness. |
waitForSelector times out |
Selector is wrong, element is in a frame, or the page never reached that state. | Inspect the live DOM, check frame boundaries, and confirm the marker’s state transition. |
| Old results satisfy the wait immediately | The predicate checks for presence but not completion of the current request. | Wait for a request-specific token, changed status, cleared-then-set marker, or updated result value. |
| PDF differs from the browser view | PDF uses print media styles by default. | Review print CSS or call emulateMediaType('screen') for screen-styled output. |
| PDF is missing custom fonts | Fonts failed to load, are blocked, or the page never completed the needed rendering. | Check font network responses and CSS; PDF generation waits for fonts by default but cannot repair failed font delivery. |
setContent() rejects a lifecycle option |
Puppeteer options vary by API and release; navigation options are not necessarily valid for setContent(). |
Check the installed version’s setContent API instead of copying goto() options. |
| Browser remains open after a failed job | Cleanup was skipped on an exception. | Place browser.close() in finally, as in the examples. |
The Puppeteer docs surfaced a version-sensitive change: a changelog entry dated 2026-05-06 says network-idle options were removed from setContent. The cited API results also label different releases. Validate behavior against the version installed in your project; do not assume goto() lifecycle options transfer to setContent().
8. Performance, reliability, and cost
Every wait adds latency, while an overly short wait risks incomplete output. Prefer the narrowest trustworthy readiness condition. A selector for the report state avoids waiting for unrelated analytics or background polling; a network idle wait can be useful when there is no better signal. Set explicit bounds so an unusual page does not occupy a worker indefinitely.
For throughput, reuse a browser process across jobs where your deployment model permits it, but give each job its own page and close pages reliably. Bound concurrent pages to the capacity of the host. These are operational practices rather than Puppeteer performance guarantees; measure memory, CPU, and job duration in your own environment. Large full-page documents, heavy client rendering, and embedded assets can increase PDF generation time and output size.
There is no useful universal dollar cost for a self-hosted Chrome job: it depends on compute, runtime, storage, and how you operate browsers. Track successful PDFs separately from readiness failures, and do not count an incomplete artifact as a successful run. If a hosted capture API better fits the workload, compare its billing rules, options, and output format before switching.
9. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. It returns a PNG, JPEG, WebP, or PDF from one GET request. Its PDF controls include paper size, margins, landscape, and page ranges. The API also supports custom wait conditions, headers, cookies, and other capture options; see the ScreenshotNeo API docs.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
For a PDF, set the documented output-format parameter and any required page or paper options in the request. Use the same readiness principle: select a wait condition tied to the page when one is available. The API accepts familiar parameter names used by other screenshot APIs, which can make migration easier.
ScreenshotNeo accepts cookie and consent banners before capture and removes 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for ScreenshotNeo and get 1,000 screenshots a month free, with no card.
10. FAQ
Does networkidle2 mean all AJAX is finished?
No. It indicates a network activity condition. The application may still need to process a response or render its state. Prefer a completion marker tied to the result.
Should I use networkidle0 or networkidle2?
Use the lifecycle condition that fits the page’s request behavior, then validate the resulting content. Neither lifecycle state universally proves that an application is ready to print.
Do I need to wait for fonts before page.pdf()?
Puppeteer’s PDF guide says PDF generation waits for fonts by default. Investigate failed font requests if the output still uses fallback fonts.
Can I print the screen version of a page?
Yes. Call page.emulateMediaType('screen') before page.pdf(); otherwise PDF output uses print media styles by default.
What if I cannot change the application to expose a ready marker?
Wait for the most reliable observable result available, such as a completed status or a result value, and use a bounded timeout. Network idle can supplement that check when the page’s behavior makes it useful.


