If Puppeteer appears to navigate to the same URL repeatedly, first determine what is repeating: your Node.js code may call page.goto() more than once, one call may follow an HTTP redirect chain, page JavaScript may trigger client-side navigations, or request interception may be stalling or aborting a navigation. These cases look similar in logs but require different fixes.
Trace the caller, main-frame requests, redirects, URL changes, and interception decisions separately. Do not start by increasing the timeout: waitUntil changes when Puppeteer considers navigation complete; it does not stop a redirect loop or page code that keeps navigating.
What page.goto() actually does
page.goto(url, options) starts a frame navigation and returns a promise for the main resource response. If the server sends multiple redirects, the promise resolves with the response for the last redirect (Puppeteer Page.goto API). Navigation to about:blank, or to the same URL with only a different hash, returns null.
The URL you see is a separate signal. page.url() reports the page’s current URL, while page.on('framenavigated') reports frame URL transitions. History API calls such as history.pushState() also count as navigation for waitForNavigation(), even though they do not fetch a new document (Page class API). Therefore, a changing URL does not necessarily mean a new HTTP document request.
Quick wins for a faster PC:
Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →#1 Best Overall
First classify the repetition
Repeated calls from your process
A loop, retry wrapper, queue consumer, test hook, or two code paths may invoke goto() repeatedly. If your counter increases once per call, the bug is in application control flow rather than the website.
One call, several HTTP requests
A single navigation can produce a chain such as / → /login → /. Record every navigation request and response to distinguish normal redirects from a loop.
Page-side navigation
Scripts can assign location, submit a form, call history.pushState(), or use a client router. These transitions can happen after goto() has started or resolved.
A wait that appears hung
waitForNavigation() can wait for an action-triggered transition, while an intercepted request can remain unresolved indefinitely. A stuck wait is not proof that goto() is repeatedly called.
Instrument the call site and main frame
Start with a narrow trace. Adapt event names and logging to the Puppeteer version installed in your project; this is diagnostic instrumentation, not a guaranteed fix.
let gotoCount = 0;
page.on('request', request => {
if (request.isNavigationRequest()) {
console.log('navigation request', {
mainFrame: request.frame() === page.mainFrame(),
url: request.url(),
method: request.method()
});
}
});
page.on('framenavigated', frame => {
if (frame === page.mainFrame()) {
console.log('main frame navigated', frame.url());
}
});
const requestedUrl = inputUrl;
console.log('goto start', ++gotoCount, requestedUrl, 'current:', page.url());
const response = await page.goto(requestedUrl, { waitUntil: 'domcontentloaded' });
console.log(
'goto resolved',
response?.url() ?? null,
response?.status() ?? null,
'current:', page.url()
);
Compare three counts: the number of goto start lines, navigation-request events, and main-frame URL transitions. If the first count is high, search callers and retries. If it is one but the other counts are high, inspect redirects or page code. If requests stop without a resolution, inspect interception.
Log the exact input
Log the string immediately before each call, including query parameters and fragments. A tiny difference such as a trailing slash, URL-encoded value, or changing token can make an apparently identical URL a different request. Also log the current page.url() so you can see whether your code is navigating back to an earlier state.
Capture errors without hiding them
try {
const response = await page.goto(requestedUrl, {
waitUntil: 'domcontentloaded',
timeout: 30_000
});
console.log('status', response?.status() ?? null);
} catch (error) {
console.error('goto failed', {
name: error.name,
message: error.message,
current: page.url()
});
throw error;
}
Do not catch an error, immediately retry the same URL, and then interpret the retry as Puppeteer’s spontaneous navigation. Add a bounded retry policy with a reason and attempt number if retries are intentional.
Free tools Windows power users keep installed
One-click scans. No signup required.
Separate redirects from client-side navigation
HTTP redirect chains
Use request and response logging to identify status codes such as 301, 302, 307, and 308. A server or proxy may redirect HTTP to HTTPS, add or remove a slash, enforce a locale, or send unauthenticated users to a login page. If two endpoints redirect to each other, Puppeteer will keep following the chain until the browser’s navigation limits or timeout are reached.
Inspect the final response URL and status, then reproduce the URL with a normal browser or an HTTP client. Check proxy rules, authentication cookies, canonical-host settings, and environment-specific base URLs. Fix the redirect source; changing waitUntil only changes waiting semantics.
Client-side redirects and routers
After the document loads, page JavaScript may navigate to the same path or repeatedly replace it. Add a framenavigated listener and, when permitted for debugging, inspect the page’s console output:
page.on('console', message => {
console.log('page console', message.type(), message.text());
});
Look for timers, authentication guards, service-worker logic, route normalization, or code that assigns window.location on every render. A closed Puppeteer issue reported a hang after a handler aborted a main-frame client-side redirect; it was opened on October 27, 2022 and discussed versions 16.1.1 through 19.2.0 (issue #9175). That report is a historical diagnostic lead, not evidence of a universal defect in current Puppeteer.
Recommended Free Tools
Rank #3
History API changes
pushState and replaceState can change page.url() without requesting a new document. If your code treats every URL change as a signal to call goto(), it can create a feedback loop: navigation changes the URL, your watcher sees the URL, and the watcher starts another navigation. Track whether a transition generated a network request before issuing a new goto().
Audit request interception
When interception is enabled with page.setRequestInterception(true), every request stalls until it is continued, responded to, aborted, or satisfied from browser cache. The official Request Interception guide warns that each request must reach exactly one resolution path.
Use a synchronous resolution guard
page.on('request', request => {
if (request.isInterceptResolutionHandled()) return;
// Apply narrowly scoped rules here.
request.continue();
});
Multiple listeners can compete. One may abort while another tries to continue, producing an already-handled error or an apparently stalled page. Remove duplicate listeners when reusing a page, and make filtering explicit for document requests, images, analytics, and other resources.
Be careful with asynchronous handlers
The guide notes that an asynchronous wait creates a race: another handler can resolve the request while your handler is awaiting. If asynchronous work is unavoidable, check isInterceptResolutionHandled() again immediately before resolving, and keep that check and the action together synchronously:
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
page.on('request', async request => {
if (request.isInterceptResolutionHandled()) return;
const shouldBlock = await decideWhetherToBlock(request.url());
if (request.isInterceptResolutionHandled()) return;
if (shouldBlock) request.abort();
else request.continue();
});
Prefer a synchronous, narrow rule for navigation requests. Blocking a stylesheet or tracker is different from aborting the main-frame request that carries a client-side redirect. Test navigation requests separately from subresources.
Resolve every branch
Code such as if (isAd(request)) request.abort(); followed by no else leaves non-ad requests pending. Make the default action explicit, and ensure exceptions in your handler cannot leave a request unresolved. If an external helper installs its own listener, inspect its behavior before adding another interceptor.
Pair actions with waitForNavigation() safely
Use waitForNavigation() when an action such as a click is expected to cause navigation, not as a general replacement for goto(). Register the wait before triggering the action and await both promises to avoid the race in which navigation finishes before the wait is installed:
const navigation = page.waitForNavigation({ waitUntil: 'domcontentloaded' });
await page.click('a.next');
await navigation;
console.log('arrived at', page.url());
If the click sometimes performs an in-page History API transition and sometimes a full load, define what outcome your test needs and observe both URL and network events. A navigation wait can resolve for a History API URL change even though no new document was fetched.
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 waitUntil only after tracing activity
waitUntil: 'load', 'domcontentloaded', and network-idle conditions control when Puppeteer resolves a navigation promise. A network-idle condition can remain pending on pages with long-lived connections, polling, or continuously requested resources. It cannot suppress a redirect loop or stop page JavaScript from navigating again. Once logs show the intended navigation is occurring, choose the least restrictive condition that matches your task, and set a timeout appropriate to the page rather than using a larger timeout to conceal a loop.
A practical decision table
| Evidence in logs | Most likely area | Next check |
|---|---|---|
| Several “goto start” lines for one job | Caller, retry, queue, or test hook | Search every call site and log attempt ownership. |
| One call, repeated 3xx navigation requests | HTTP redirect or proxy loop | Record status, Location target, cookies, and host normalization. |
| Few network requests, many frame URL changes | Client router or History API | Inspect page scripts and prevent URL observers from calling goto recursively. |
| Requests stop while interception is enabled | Unresolved or multiply resolved request | Audit every listener and add the synchronous resolution guard. |
| Action wait hangs or resolves unexpectedly | Incorrect wait pairing or event race | Install wait first and verify whether the action causes a document load. |
Troubleshooting common errors
“Navigation timeout exceeded”
First determine whether requests are looping, the page is intentionally busy, or interception left a request stalled. Log the redirect sequence and unresolved requests. Only then adjust timeout or waitUntil.
“Request is already handled”
Two listeners resolved the same request, or an asynchronous listener resumed after another listener acted. Consolidate handlers, guard every path, and re-check immediately before resolution.
The final URL is the original URL
The server may redirect back after authentication, a cookie may be missing, or client code may normalize the route. Compare response URL, status, cookies, and frame events instead of assuming goto() called itself.
The script works with interception disabled
Enable handlers one at a time. Confirm that every request is continued, responded to, or aborted exactly once, and avoid broad abort rules for main-frame requests.
Repeated navigation starts after a click
Check whether the click handler itself submits twice, whether a framework retries the route, or whether your test calls the action again after a failed wait. Register the wait before the click and log the click attempt.
What to include when asking for help
- The installed Puppeteer version and browser revision.
- A minimal script containing the requested URL and the exact
gotoor action/wait pair. - Every request-interception listener, including helper packages.
- Ordered logs showing caller count, navigation requests, response status and URL, frame navigations, and errors.
- Whether the behavior changes when interception, extensions, proxies, or authentication are disabled.
This information lets others distinguish a caller loop from a server redirect, page-side navigation, or interception race instead of guessing from a timeout message.
Or skip the browser setup
If your goal is simply to obtain a clean website image or PDF rather than debug a browser workflow, ScreenshotNeo provides a GET-based screenshot API. For example:
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 and options in the ScreenshotNeo documentation. ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account to try it without a card.
Frequently Asked Questions
Does page.goto() call itself when a site redirects?
No. One call can follow multiple server redirects, while page scripts or your own code can initiate additional navigations. Log the caller and navigation events separately.
Should I always use networkidle to fix a hanging navigation?
No. Network-idle is only a completion condition. Diagnose redirects, page-side navigation, and request interception before changing it.
The Tool Desk
Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Can a History API change trigger waitForNavigation()?
Yes. Puppeteer treats History API URL changes as navigation for waitForNavigation(), even without a new document request.
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.

