Skip to main content

Best Practices

This page covers essential practices for creating robust Puppeteer and Playwright scripts with Browserless. Follow these patterns to avoid common pitfalls, improve performance, and keep your automation code maintainable.

Concurrency limits

Always close your browser sessions properly to avoid hitting concurrency limits:

import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE`,
});

try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

// Your automation logic here

} catch (error) {
console.error('Automation failed:', error.message);
} finally {
// Always close the browser, even on errors
if (browser.isConnected()) {
await browser.close();
}
}

Concurrency limits by plan

PlanConcurrency (Monthly)Concurrency (Yearly)
Free22
Prototyping510
Starter3040
Scale80100
EnterpriseCustomCustom

Timeouts

Session timeouts

Control the maximum duration of a browser session using the timeout parameter in your connection URL. The value is in milliseconds.

import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE&timeout=300000`,
// 300000ms = 5 minutes
});

Session duration limits

Maximum session duration depends on your subscription plan:

PlanMaximum Session Duration
Free1 minute
Prototyping (20k)15 minutes
Starter (180k)30 minutes
Scale (500k) and above60 minutes
Enterprise (self-hosted)Custom

To increase your limit, see the pricing page.

Avoid network latency

Call the nearest regional endpoint to reduce network latency and improve performance. Using a geographically closer endpoint significantly reduces connection and data transfer time. For available regional endpoints and load balancing options, see Load Balancers.

Reduce Network Round-trips

Every await call (e.g. page.click(), page.textContent()) is a separate WebSocket round-trip between your client and the remote browser. Five sequential await calls means five round-trips, each adding network latency. Wrapping the same operations in a single page.evaluate() executes them all inside the browser in one round-trip, returning the results together. The difference scales linearly with the number of operations.

// DON'T DO - Multiple round-trips
const button = await page.$('.buy-now');
const buttonText = await button.getProperty('innerText');
const isVisible = await button.isIntersectingViewport();
await button.click();

// DO - Single round-trip
const result = await page.evaluate(() => {
const button = document.querySelector('.buy-now');
const buttonText = button.innerText;
const isVisible = button.offsetParent !== null;
button.click();

return { buttonText, isVisible, clicked: true };
});

Wait for the page to be ready

When is your page fully ready? It depends on the site. Some pages are usable after the initial DOM load, while others need async data or scripts to finish executing. Here are the signals you can wait for:

  • Load events (commit, load, domcontentloaded, networkidle)
  • Specific selectors to appear or become visible
  • Network requests to complete
  • Custom events fired by the page

Wait Until the Page has Loaded

The waitUntil parameter in page.goto() controls when navigation is considered complete. Choose based on what your target content requires:

OptionAvailabilityFires whenBest for
commitPlaywright onlyResponse headers are parsed and session history is updated, before the document starts loadingFastest possible signal; useful when you only need to confirm navigation started
domcontentloadedPuppeteer, PlaywrightDocument content is loaded and parsedFast pages, static content, SPAs with client-side rendering
loadPuppeteer, PlaywrightPage executes scripts and loads resources like stylesheets and imagesPages where visual completeness matters
networkidle2Puppeteer onlyNo more than 2 network connections for at least 500msSPAs, pages with async data fetching
networkidle / networkidle0Playwright (networkidle) / Puppeteer (networkidle0)Zero network connections for at least 500msStrictest check, slowest option

Start with domcontentloaded and only move to networkidle2/networkidle if your target content requires it. Using networkidle0/networkidle on busy pages can cause unnecessary timeouts. Use commit (Playwright only) when you need the earliest possible signal that navigation occurred.

For a deeper dive, see our waitUntil blog post.

import puppeteer from "puppeteer-core";

const browser = await puppeteer.connect({
browserWSEndpoint: `wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE`,
});

const page = await browser.newPage();

// Fast navigation - use when you only need DOM content
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 30000
});

// Use networkidle2 only when you need all resources loaded
await page.goto('https://spa-app.com', {
waitUntil: 'networkidle2',
timeout: 60000
});

Wait for Specific Selectors

Waiting for specific selectors is one of the most reliable ways to ensure your target content is ready. This approach works well for dynamic content that loads asynchronously.

// Wait for a specific element to appear
await page.waitForSelector('.dynamic-content');

// Wait for element to be visible
await page.waitForSelector('.loading-spinner', { hidden: true });

// Wait for multiple elements
await page.waitForSelector('.product-list .product-item');

Wait for Network Requests

Sometimes you need to wait for specific network requests to complete before proceeding. This is useful when your target data comes from API calls.

// Wait for a specific request to complete
await page.waitForResponse(response =>
response.url().includes('/api/data') && response.status() === 200
);

// Wait for all requests to finish
await page.waitForLoadState('networkidle');

// Wait for multiple requests
await Promise.all([
page.waitForResponse(response => response.url().includes('/api/users')),
page.waitForResponse(response => response.url().includes('/api/posts'))
]);

Wait for Custom Events

Many modern web applications fire custom events when specific actions complete. You can listen for these events to know exactly when your target functionality is ready.

// Wait for a custom event
await page.evaluate(() => {
return new Promise((resolve) => {
document.addEventListener('dataLoaded', resolve, { once: true });
});
});

// Wait for multiple custom events
await Promise.all([
page.evaluate(() => new Promise(resolve =>
document.addEventListener('userDataReady', resolve, { once: true })
)),
page.evaluate(() => new Promise(resolve =>
document.addEventListener('contentReady', resolve, { once: true })
))
]);

HTTP error codes

CodeCauseHow to resolve
400 Bad RequestMalformed JSON, invalid fields, negative or out-of-range timeout, or colliding request argumentsValidate your payload structure and check for conflicting parameters
401 UnauthorizedMissing or invalid API key, or endpoint not supported by your planCheck that your token is correct and that your plan supports the endpoint
403 ForbiddenWrong regional endpoint or insufficient permissions. The deprecated chrome.browserless.io endpoint returns 403. Use production-sfo.browserless.io instead.Verify you're using the correct regional endpoint for your account
404 Not FoundEndpoint does not existCheck your endpoint URL
408 Request TimeoutTimeout set too low, waiting for a selector or event that never appears, or session exceeded plan limitIncrease timeout, verify your selectors exist, check session duration limits above
429 Too Many RequestsExceeding your plan's concurrency limit, usually from unclosed sessions accumulatingClose browser sessions in finally blocks; upgrade your plan if needed

Failed requests appear as "Rejected" sessions in your account dashboard. To check the HTTP status code of a site navigated to by a REST API, look for the x-response-code and x-response-status response headers.

Getting Help

If you continue to experience issues after implementing these best practices:

  1. Check your account dashboard for usage metrics
  2. Contact Browserless support for assistance

Next Steps