How to Wait for Iframes Before Generating PDFs with Puppeteer
Wait for the target Puppeteer frame and its real ready state before calling page.pdf(), including navigation-safe code and troubleshooting.
To generate a PDF after iframe content is ready, obtain the iframe’s Puppeteer Frame, wait for an application-specific completion signal inside that frame, and only then call page.pdf(). If an action navigates the iframe, register frame.waitForNavigation() before triggering the action and await both operations together.
An iframe has its own document and Puppeteer context. A selector wait on the outer page does not prove that the embedded application has finished loading or rendering its data.
Complete example: wait for a named iframe and create a PDF
This runnable Node.js example waits for an iframe whose name is report, then waits for a visible application marker before printing. Replace the frame identity and readiness selector with values from the page you automate.
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
});
try {
const page = await browser.newPage();
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
timeout: 30_000,
});
const frame = await page.waitForFrame(async (candidate) => {
const element = await candidate.frameElement();
if (!element) return false;
return await element.evaluate((el) => el.getAttribute('name') === 'report');
});
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 30_000,
});
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
});
} finally {
await browser.close();
}
Page.waitForFrame() accepts a URL or predicate. Frame.waitForSelector() waits inside that frame and works across frame navigations. The completion marker must represent finished application work; the existence of an iframe or a generic container can occur before its data is rendered.
Install Puppeteer and run the example
mkdir iframe-pdf && cd iframe-pdf
npm init -y
npm install puppeteer
Save the example as index.mjs and run:
node index.mjs
Puppeteer downloads a compatible browser during installation. In a controlled environment where Chromium is already installed, pass its path with puppeteer.launch({ executablePath: '/path/to/chromium' }).
Identify the correct iframe
Match a stable name or attribute
A stable attribute is preferable when several frames exist:
const frame = await page.waitForFrame(async (candidate) => {
const element = await candidate.frameElement();
return Boolean(element && await element.evaluate(
(el) => el.getAttribute('data-testid') === 'report-frame'
));
});
Match the frame URL
const frame = await page.waitForFrame(
(candidate) => candidate.url().startsWith('https://reports.example.com/')
);
A URL predicate is useful when the embedded service has a documented, stable origin. Avoid matching only the first child frame when the page contains analytics, payment, chat, or other unrelated frames.
Inspect the current frame tree
for (const frame of page.frames()) {
console.log({ url: frame.url(), name: frame.name() });
for (const child of frame.childFrames()) {
console.log({ childUrl: child.url(), childName: child.name() });
}
}
Use page.frames() and frame.childFrames() when the frame already exists and you need to inspect its structure. For asynchronously created iframes, page.waitForFrame() expresses the wait directly.
Wait for the iframe’s real ready state
Prefer an application completion marker
The most reliable signal is one the embedded application sets after data, charts, and other asynchronous work finish:
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 30_000,
});
The visible option requires the element to be present and visible. Set a timeout that matches the expected job duration and treat a timeout as a failed capture instead of producing an incomplete PDF.
Wait for a condition with waitForFunction
await frame.waitForFunction(
() => window.reportState && window.reportState.status === 'complete',
{ timeout: 45_000 }
);
Use a condition when the application exposes state but no useful DOM marker. Keep the predicate narrow and deterministic.
Wait for a selector, then verify its content
await frame.waitForSelector('#report-table', {
visible: true,
timeout: 30_000,
});
await frame.waitForFunction(() => {
const table = document.querySelector('#report-table');
return table && table.querySelectorAll('tbody tr').length > 0;
}, { timeout: 30_000 });
Selector presence alone can be too early when the application inserts an empty shell before fetching rows.
Use a short delay only as a last resort
await new Promise((resolve) => setTimeout(resolve, 2_000));
A fixed delay is simple but fragile: slow runs can still be incomplete, while fast runs waste time. Prefer a marker or state condition that describes completion.
Handle iframe navigation without a race
If a click or form submission navigates the frame, start the navigation wait before the action. Puppeteer’s coordinated pattern is Promise.all:
const [response] = await Promise.all([
frame.waitForNavigation({ timeout: 30_000 }),
frame.click('a.generate-report'),
]);
console.log('navigation response:', response ? response.status() : null);
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 30_000,
});
await page.pdf({ path: 'report.pdf' });
Registering waitForNavigation() after the click can miss a fast navigation. The wait resolves with the main-resource response or null; History API URL changes also count as navigation. Navigation completion still does not guarantee that client-side data rendering is finished, so follow it with the application’s ready signal.
When the action does not navigate
For an in-place update, do not wait for navigation. Click and then wait for the state that proves the update completed:
await frame.click('button.refresh');
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 30_000,
});
Generate the PDF with the right print settings
page.pdf() uses print CSS media by default and waits for fonts by default. Choose options that match the document:
await page.pdf({
path: 'report.pdf',
format: 'A4',
landscape: false,
printBackground: true,
preferCSSPageSize: true,
margin: {
top: '12mm',
right: '12mm',
bottom: '12mm',
left: '12mm',
},
});
| Option | Use it when |
|---|---|
format |
You want a standard paper size such as A4 or Letter. |
preferCSSPageSize |
The document’s @page rules should control dimensions. |
landscape |
Wide tables or charts need horizontal pages. |
margin |
You need predictable printable whitespace. |
printBackground |
Background colors, fills, or images are part of the report. |
pageRanges |
Only selected pages should be emitted. |
Use screen media when the design requires it
await page.emulateMediaType('screen');
await page.pdf({
path: 'screen-styled-report.pdf',
printBackground: true,
});
Use screen media deliberately. Print media may apply different layout rules, hide navigation, or change colors.
Cross-origin and nested iframe considerations
Puppeteer can wait in a cross-origin frame because it controls the browser context, but page JavaScript cannot freely read another origin’s DOM. Run selectors and functions through the matching Puppeteer Frame, not through an outer-page evaluate call that assumes same-origin access.
For nested frames, identify the parent first and then inspect parent.childFrames(). Wait on the deepest frame that owns the readiness marker.
If the iframe is sandboxed, blocked by a content-security policy, protected by authentication, or denied by the provider, no selector strategy can make unavailable content appear. Fix access and credentials first.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
waitForFrame times out |
The frame is created later, its URL or attribute differs, or it was blocked. | Log page.frames(), match a stable attribute or URL, and inspect console, request, and response failures. |
| Outer selector wait succeeds but PDF lacks iframe data | The wait ran in the outer document or matched an empty shell. | Call frame.waitForSelector() or frame.waitForFunction() with the application’s completion condition. |
| PDF contains the old iframe page | A navigation wait was registered after the action or the action was not awaited. | Use Promise.all([frame.waitForNavigation(), action]), then wait for the post-navigation ready marker. |
| Navigation resolves but report is incomplete | Network navigation finished before client-side rendering. | Wait for a completed status, populated row count, chart marker, or other application signal. |
| Selector timeout | The marker is wrong, hidden, inside another nested frame, or slower than the timeout. | Verify the frame tree and selector manually, then increase the timeout only to the job’s real upper bound. |
| Fonts or backgrounds are missing | Print CSS, font loading, or background printing changed output. | Wait for the application state, use emulateMediaType('screen') when appropriate, and set printBackground: true. |
| Intermittent blank or partial PDFs | Fixed delays, flaky dependencies, or resource failures. | Replace sleeps with state waits, capture browser errors, retry only transient failures, and fail clearly on readiness timeout. |
Reliability and performance checklist
- Identify frames by stable names, attributes, or URL predicates.
- Wait inside the owning frame, not only on the outer page.
- Use an application-specific completion signal.
- Pair navigation-triggering actions and waits in
Promise.all. - Set explicit timeouts and record which wait failed.
- Reuse a browser process for batches, while creating an isolated page per PDF job.
- Close pages in a
finallyblock and close the browser during shutdown. - Record frame URLs, navigation responses, console errors, request failures, and PDF duration for diagnosis.
- Do not retry deterministic selector errors indefinitely; retry only known transient network or browser failures.
Or skip the browser setup
ScreenshotNeo provides a website capture API and PDF endpoint when you do not want to manage Chromium, iframe waits, and print configuration. See the ScreenshotNeo API documentation for request options.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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}`);
Before capture, ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed as clean shots; response headers report the page verdict and billing result. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month with no card, and paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots per month without a card.
FAQ
Does waiting for load guarantee iframe content is ready?
No. The iframe’s document can finish loading before API data, charts, or client-side components render. Wait for the embedded application’s completion signal.
Can I call page.pdf() on the iframe itself?
No. PDF generation is a Page operation. Wait on the target Frame, then call page.pdf() for the page containing it.
What if the iframe never exposes a ready marker?
Coordinate with the application owner to add a stable marker or state value. As a fallback, combine a meaningful content check with a bounded timeout; avoid unbounded sleeps.
Why does the PDF differ from the browser view?
Puppeteer prints with print media by default. Print CSS, paper dimensions, margins, backgrounds, and page-break rules can all change the result. Use screen media and explicit PDF options when that is the intended output.
How do I know which Puppeteer behavior my package supports?
Check the API reference for the Puppeteer version installed in your project. The exact surfaced documentation versions can differ, so pin and review the package version used by your deployment.


