Wait for the condition that proves your AJAX result is usable, not for an arbitrary delay. For a result element inserted after a request, use await page.waitForSelector('.results', { visible: true }). If the container exists before its contents arrive, wait for a predicate such as non-empty text or a minimum item count with waitForFunction. For clicks and form actions, Puppeteer’s current locator API usually performs the necessary waiting itself.
The right wait depends on what “ready” means
AJAX updates happen after the initial document load. A page can report that navigation is complete while JavaScript is still fetching data and changing the DOM. Define the observable state your next operation needs:
- Element inserted: wait for a specific selector.
- Element visible: wait for the selector with
visible: true. - Loading indicator gone: wait for the spinner with
hidden: true, then verify the result. - Container exists but is empty: wait for a content predicate with
waitForFunction. - About to interact: use a locator action such as
page.locator(selector).click()or.fill(). - Network quietness is genuinely relevant: use
waitForNetworkIdle, while remembering that network inactivity does not prove semantic readiness.
Set up Puppeteer with a compatible browser
The current Puppeteer system-requirements guide lists Node.js 22.12 or newer and supported Chrome for Testing platform combinations. Install the full puppeteer package when you want its compatible browser download:
npm install puppeteer
puppeteer-core does not download a browser. You must provide an executable path, and package managers that disable install scripts can also prevent the automatic Chrome download. Resolve launch and installation problems before diagnosing a selector wait.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →#1 Best Overall
Basic pattern: wait for a result element
Use a selector that represents the actual result, not a generic page wrapper:
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({ headless: true });
const page = await browser.newPage();
await page.goto('https://example.com/search', { waitUntil: 'domcontentloaded' });
await page.locator('#search').fill('puppeteer');
await page.locator('#submit').click();
await page.waitForSelector('.results', { visible: true });
const titles = await page.$$eval('.results h2', nodes =>
nodes.map(node => node.textContent.trim()),
);
console.log(titles);
await browser.close();
})();
waitForSelector resolves immediately when the selector already exists. Otherwise it waits until the selector appears or the timeout expires. The documented default timeout is 30,000 milliseconds. Its current options include visible, hidden, timeout, and cancellation through an AbortSignal.
Wait for visibility, not merely presence
await page.waitForSelector('.results', { visible: true, timeout: 15000 });
Without visible: true, a matching node can be hidden by CSS. Visibility waiting requires the node to be present and visible. Set a per-call timeout when this operation has a different service-level deadline from the rest of the page.
Wait for a spinner to disappear
await page.waitForSelector('.loading', { hidden: true });
await page.waitForSelector('.results .result', { visible: true });
hidden: true succeeds when the selector is absent or hidden. A spinner can disappear after an error or an empty response, so pair it with a positive result check whenever an empty state is not valid.
When the result container exists before AJAX data
Many applications render <div class="results"></div> immediately and populate it later. Waiting for the container then returns too early. Express the data condition instead:
Rank #2
await page.waitForFunction(() => {
const results = document.querySelector('.results');
return results && results.textContent.trim().length > 0;
});
For lists, a count is often more precise:
await page.waitForFunction(() =>
document.querySelectorAll('.results .result').length > 0,
{ timeout: 20000 },
);
The predicate runs in the page context and resolves when it becomes truthy. You can configure polling, timeout, and cancellation. Prefer a condition tied to the business result—for example, a known status value or a required number of rows—rather than merely waiting longer.
Use locators for actions
Current Puppeteer guidance recommends locators for interactions. They wait for action preconditions such as presence, visibility, enabled state, viewport presence, and a stable layout when relevant:
await page.locator('#search').fill('puppeteer');
await page.locator('#submit').click();
await page.locator('.results .next-page').click();
This removes a separate “is the button there?” wait when the goal is to act on that button. Use an explicit selector or predicate after the action when you need to extract data.
Free tools Windows power users keep installed
One-click scans. No signup required.
Coordinate waits with navigation
If a click causes navigation, start the navigation wait and the click together so a fast transition cannot be missed:
await Promise.all([
page.waitForNavigation({ waitUntil: 'domcontentloaded' }),
page.locator('a.next').click(),
]);
For a true AJAX update with no navigation, do not use navigation completion as the readiness signal. Click first, then wait for the result selector or data predicate.
Rank #3
When (and when not) to use network idle
await page.waitForNetworkIdle({ idleTime: 500 });
The documented default idle interval is 500 ms, and Puppeteer states that the function always waits at least the configured idle time. This observes network inactivity across the page; it does not inspect whether a particular result contains the expected data.
Network-idle waits can be a poor fit for pages with analytics, long polling, streaming, advertisements, or unrelated background requests. They may hang while the page is otherwise ready, or resolve while an application still has to render data. If you use one, combine it with a result check:
await page.waitForNetworkIdle({ idleTime: 500 });
await page.waitForFunction(() =>
document.querySelectorAll('.results .result').length > 0,
);
Selector and page-context edge cases
Wrong selector or an untriggered request
A timeout commonly means the selector is incorrect, the click or request never happened, or the application returned an error or empty state. Confirm the selector in DevTools, log the page URL, and inspect the rendered HTML after the action.
Frames
Selectors are evaluated in the current page context. If the AJAX content is inside an iframe, obtain the matching frame and wait there:
const frame = page.frames().find(f => f.url().includes('/results-frame'));
if (!frame) throw new Error('Results frame was not found');
await frame.waitForSelector('.result', { visible: true });
Shadow DOM
Modern Puppeteer selector support includes combinations for shadow roots, as well as text, accessibility, and XPath selectors. Use a selector that reaches the component’s shadow tree, or expose a page-context predicate that queries the relevant root when the component’s structure is application-specific.
Empty, error, and “no results” states
Make failure states explicit so a legitimate empty search does not become a 30-second timeout:
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 & 11await page.waitForFunction(() => {
const ready = document.querySelector('.results .result');
const empty = document.querySelector('.results-empty');
const error = document.querySelector('.results-error');
return Boolean(ready || empty || error);
}, { timeout: 20000 });
Afterward, branch on which state is present and report an application error separately from a transport or selector error.
Timeouts, cancellation, and diagnostics
The default selector timeout is 30 seconds. Override it locally or configure a page-wide default for a consistent policy:
page.setDefaultTimeout(15000);
await page.waitForSelector('.results', { timeout: 10000 });
timeout: 0 disables the timeout; use that only when an external watchdog will terminate the job. For cancellable workflows, pass an abort signal where supported:
const controller = new AbortController();
setTimeout(() => controller.abort(), 10000);
await page.waitForSelector('.results', { signal: controller.signal });
Capture evidence when a wait fails:
try {
await page.waitForSelector('.results .result', { visible: true, timeout: 15000 });
} catch (error) {
console.error('URL:', page.url());
console.error('Title:', await page.title());
await page.screenshot({ path: 'wait-failure.png', fullPage: true });
throw error;
}
Check whether a consent dialog, login wall, bot challenge, or error overlay prevented the request. Also verify that the content is not in a different frame or shadow root.
Best Value
Performance and reliability practices
- Use the narrowest selector and the smallest meaningful predicate; broad DOM scans run more often and are harder to diagnose.
- Start a wait before an action when that action can replace the target or trigger navigation.
- Prefer one semantic readiness condition over a chain of arbitrary sleeps.
- Use a bounded timeout and record whether the outcome was success, empty, application error, or timeout.
- Do not add
setTimeoutsleeps as the primary synchronization mechanism. A slow machine or network can exceed the sleep, while a fast response wastes time. - Keep browser setup separate from wait logic; a missing executable or blocked install script is not an AJAX timing problem.
Or skip the browser setup
If your goal is a clean image or PDF rather than interactive scraping, ScreenshotNeo provides a single HTTP request. It accepts cookie and consent banners as a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be disabled. Only clean shots are billed: bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status.
For example, the API call below returns a WebP screenshot:
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 complete parameter reference in the ScreenshotNeo documentation. The same request in 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)
And in 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(`HTTP ${res.status}`);
const fs = require('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. It supports full-page and element captures, device and viewport settings, retina scale, custom CSS and JavaScript, clicks, waits, request blocking, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous webhooks, bulk capture of up to 100 URLs per call, usage reporting, and an OpenAPI specification. Every feature is included on every plan. The Free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Create a free ScreenshotNeo account.
Recommended Free Tools
Quick decision guide
| Situation | Use | What it proves |
|---|---|---|
| Result node is inserted after the request | waitForSelector(selector) |
The node exists |
| Node must be displayed | waitForSelector(selector, { visible: true }) |
The node exists and is visible |
| Spinner should finish | waitForSelector(spinner, { hidden: true }) plus a result check |
Loading UI ended; separate check proves success |
| Container is present but data is delayed | waitForFunction(predicate) |
Your content condition is true |
| Click or fill operation | page.locator(...).click() or .fill() |
Action preconditions are met |
| Only network quietness matters | waitForNetworkIdle() |
Network has been idle for the configured interval |
Frequently Asked Questions
Can I wait for a specific API response instead of the DOM?
Yes. Listen for the request or response that represents the operation, then still verify the rendered state if your next step reads the page. A successful HTTP response alone does not guarantee that the framework has committed the data to the DOM.
Why does my wait pass immediately?
The selector may already match a shell element rendered before the AJAX data arrives. Replace the presence wait with a predicate for text, item count, status, or another value that cannot be true until the result is usable.
Should I increase the timeout when a wait fails?
Only after checking the selector, trigger, frame, shadow root, authentication state, and error/empty branches. A longer timeout cannot correct a selector that never appears or a request that never ran.
The Bottom Line
In Puppeteer, synchronize with the application state your script needs: selector for insertion, visibility for display, a predicate for populated data, locators for actions, and network idle only when network quietness is itself meaningful.
Do these 3 things before closing this tab:
1Fix the driver behind crashes, sound loss and screen glitches2Repair Windows errors before they cause bigger problems3Scan for outdated or missing drivers - takes under a minuteQuick 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.




