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:
- Puppeteer
- Playwright
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();
}
}
import playwright from "playwright";
const browser = await playwright.chromium.connectOverCDP(
`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
| Plan | Concurrency (Monthly) | Concurrency (Yearly) |
|---|---|---|
| Free | 2 | 2 |
| Prototyping | 5 | 10 |
| Starter | 30 | 40 |
| Scale | 80 | 100 |
| Enterprise | Custom | Custom |
Timeouts
Session timeouts
Control the maximum duration of a browser session using the timeout parameter in your connection URL. The value is in milliseconds.
- Puppeteer
- Playwright
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
});
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.connect_over_cdp(
"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:
| Plan | Maximum Session Duration |
|---|---|
| Free | 1 minute |
| Prototyping (20k) | 15 minutes |
| Starter (180k) | 30 minutes |
| Scale (500k) and above | 60 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.
- Puppeteer
- Playwright
// 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 };
});
// DON'T DO - Multiple round-trips
const button = await page.locator('.buy-now');
const buttonText = await button.textContent();
const isVisible = await button.isVisible();
await button.click();
// DO - Single round-trip
const result = await page.evaluate(() => {
const button = document.querySelector('.buy-now');
const buttonText = button.textContent;
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:
| Option | Availability | Fires when | Best for |
|---|---|---|---|
commit | Playwright only | Response headers are parsed and session history is updated, before the document starts loading | Fastest possible signal; useful when you only need to confirm navigation started |
domcontentloaded | Puppeteer, Playwright | Document content is loaded and parsed | Fast pages, static content, SPAs with client-side rendering |
load | Puppeteer, Playwright | Page executes scripts and loads resources like stylesheets and images | Pages where visual completeness matters |
networkidle2 | Puppeteer only | No more than 2 network connections for at least 500ms | SPAs, pages with async data fetching |
networkidle / networkidle0 | Playwright (networkidle) / Puppeteer (networkidle0) | Zero network connections for at least 500ms | Strictest 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.
- Puppeteer
- Playwright
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
});
from playwright.async_api import async_playwright
async with async_playwright() as p:
browser = await p.chromium.connect_over_cdp(
"wss://production-sfo.browserless.io/?token=YOUR_API_TOKEN_HERE"
)
page = await browser.new_page()
# Fast navigation - use when you only need DOM content
await page.goto('https://example.com',
wait_until='domcontentloaded',
timeout=30000)
# Use networkidle only when you need all resources loaded
await page.goto('https://spa-app.com',
wait_until='networkidle',
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.
- Puppeteer
- Playwright
// 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 a specific element to appear
await page.wait_for_selector('.dynamic-content')
# Wait for element to be visible
await page.wait_for_selector('.loading-spinner', state='hidden')
# Wait for multiple elements
await page.wait_for_selector('.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.
- Puppeteer
- Playwright
// 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 a specific request to complete
async with page.expect_response(lambda response: '/api/data' in response.url and response.status == 200):
pass
# Wait for all requests to finish
await page.wait_for_load_state('networkidle')
# Wait for multiple requests
await asyncio.gather(
page.wait_for_response(lambda response: '/api/users' in response.url),
page.wait_for_response(lambda response: '/api/posts' in response.url)
)
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.
- Puppeteer
- Playwright
// 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 })
))
]);
# Wait for a custom event
await page.evaluate("""
() => new Promise((resolve) => {
document.addEventListener('dataLoaded', resolve, { once: true });
})
""")
# Wait for multiple custom events
await asyncio.gather(
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
| Code | Cause | How to resolve |
|---|---|---|
| 400 Bad Request | Malformed JSON, invalid fields, negative or out-of-range timeout, or colliding request arguments | Validate your payload structure and check for conflicting parameters |
| 401 Unauthorized | Missing or invalid API key, or endpoint not supported by your plan | Check that your token is correct and that your plan supports the endpoint |
| 403 Forbidden | Wrong 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 Found | Endpoint does not exist | Check your endpoint URL |
| 408 Request Timeout | Timeout set too low, waiting for a selector or event that never appears, or session exceeded plan limit | Increase timeout, verify your selectors exist, check session duration limits above |
| 429 Too Many Requests | Exceeding your plan's concurrency limit, usually from unclosed sessions accumulating | Close 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:
- Check your account dashboard for usage metrics
- Contact Browserless support for assistance