16 KiB
Session Management Guide
Complete guide to browser session management for performance optimization and concurrency handling.
Why Session Management Matters
The Problem:
- Launching new browsers is slow (~2-3 seconds cold start)
- Each launch consumes concurrency quota
- Free tier: Only 3 concurrent browsers
- Paid tier: 10-30 concurrent browsers (costs $2/browser beyond included)
The Solution:
- Reuse browser sessions across requests
- Use multiple tabs instead of multiple browsers
- Check limits before launching
- Disconnect (don't close) to keep sessions alive
Benefits:
- ⚡ 50-70% faster (no cold start)
- 💰 Lower costs (reduced concurrency charges)
- 📊 Better utilization (one browser, many tabs)
Session Lifecycle
1. Launch → Browser session created (session ID assigned)
2. Connected → Worker actively using browser
3. Disconnected → Session idle, available for reuse
4. Timeout → Session closed after 60s idle (configurable)
5. Closed → Session terminated (must launch new one)
Session States
| State | Description | Can Connect? |
|---|---|---|
| Active with connection | Worker is using browser | ❌ No (occupied) |
| Active without connection | Browser idle, waiting | ✅ Yes (available) |
| Closed | Session terminated | ❌ No (gone) |
Session Management API
puppeteer.sessions()
List all currently running browser sessions.
Signature:
await puppeteer.sessions(binding: Fetcher): Promise<SessionInfo[]>
Response:
interface SessionInfo {
sessionId: string; // Unique session ID
startTime: number; // Unix timestamp (ms)
connectionId?: string; // Present if worker is connected
connectionStartTime?: number;
}
Example:
const sessions = await puppeteer.sessions(env.MYBROWSER);
// Find free sessions (no active connection)
const freeSessions = sessions.filter(s => !s.connectionId);
// Find occupied sessions
const occupiedSessions = sessions.filter(s => s.connectionId);
console.log({
total: sessions.length,
available: freeSessions.length,
occupied: occupiedSessions.length
});
Output:
[
{
"sessionId": "478f4d7d-e943-40f6-a414-837d3736a1dc",
"startTime": 1711621703708,
"connectionId": "2a2246fa-e234-4dc1-8433-87e6cee80145",
"connectionStartTime": 1711621704607
},
{
"sessionId": "565e05fb-4d2a-402b-869b-5b65b1381db7",
"startTime": 1711621703808
}
]
Interpretation:
- Session
478f4d...is occupied (has connectionId) - Session
565e05...is available (no connectionId)
puppeteer.history()
List recent sessions, both open and closed.
Signature:
await puppeteer.history(binding: Fetcher): Promise<HistoryEntry[]>
Response:
interface HistoryEntry {
sessionId: string;
startTime: number;
endTime?: number; // Present if closed
closeReason?: number; // Numeric close code
closeReasonText?: string; // Human-readable reason
}
Close Reasons:
"NormalClosure"- Explicitly closed with browser.close()"BrowserIdle"- Timeout due to 60s idle period"Unknown"- Unexpected closure
Example:
const history = await puppeteer.history(env.MYBROWSER);
history.forEach(entry => {
const duration = entry.endTime
? (entry.endTime - entry.startTime) / 1000
: 'still running';
console.log({
sessionId: entry.sessionId,
duration: `${duration}s`,
closeReason: entry.closeReasonText || 'N/A'
});
});
Use Cases:
- Monitor browser usage patterns
- Debug unexpected closures
- Track session lifetimes
- Estimate costs
puppeteer.limits()
Check current account limits and session availability.
Signature:
await puppeteer.limits(binding: Fetcher): Promise<LimitsInfo>
Response:
interface LimitsInfo {
activeSessions: Array<{ id: string }>;
maxConcurrentSessions: number;
allowedBrowserAcquisitions: number; // Can launch now?
timeUntilNextAllowedBrowserAcquisition: number; // ms to wait
}
Example:
const limits = await puppeteer.limits(env.MYBROWSER);
console.log({
active: limits.activeSessions.length,
max: limits.maxConcurrentSessions,
canLaunch: limits.allowedBrowserAcquisitions > 0,
waitTime: limits.timeUntilNextAllowedBrowserAcquisition
});
Output:
{
"activeSessions": [
{ "id": "478f4d7d-e943-40f6-a414-837d3736a1dc" },
{ "id": "565e05fb-4d2a-402b-869b-5b65b1381db7" }
],
"allowedBrowserAcquisitions": 1,
"maxConcurrentSessions": 10,
"timeUntilNextAllowedBrowserAcquisition": 0
}
Interpretation:
- 2 sessions currently active
- Maximum 10 concurrent sessions allowed
- Can launch 1 more browser now
- No wait time required
puppeteer.connect()
Connect to an existing browser session.
Signature:
await puppeteer.connect(binding: Fetcher, sessionId: string): Promise<Browser>
Example:
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);
if (freeSession) {
try {
const browser = await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
console.log("Connected to existing session:", browser.sessionId());
} catch (error) {
console.log("Connection failed, session may have closed");
}
}
Error Handling:
Session may close between .sessions() call and .connect() call. Always wrap in try-catch.
browser.sessionId()
Get the current browser's session ID.
Signature:
browser.sessionId(): string
Example:
const browser = await puppeteer.launch(env.MYBROWSER);
const sessionId = browser.sessionId();
console.log("Current session:", sessionId);
browser.disconnect()
Disconnect from browser WITHOUT closing it.
Signature:
await browser.disconnect(): Promise<void>
When to use:
- Want to reuse session later
- Keep browser warm for next request
- Reduce cold start times
Example:
const browser = await puppeteer.launch(env.MYBROWSER);
const sessionId = browser.sessionId();
// Do work
const page = await browser.newPage();
await page.goto("https://example.com");
// Disconnect (keep alive)
await browser.disconnect();
// Later: reconnect
const browserAgain = await puppeteer.connect(env.MYBROWSER, sessionId);
Important:
- Browser will still timeout after 60s idle (use
keep_aliveto extend) - Session remains in your concurrent browser count
- Other workers CAN connect to this session
browser.close()
Close the browser and terminate the session.
Signature:
await browser.close(): Promise<void>
When to use:
- Done with browser completely
- Want to free concurrency slot
- Error occurred during processing
Example:
const browser = await puppeteer.launch(env.MYBROWSER);
try {
// Do work
} catch (error) {
await browser.close(); // Clean up on error
throw error;
}
await browser.close(); // Normal cleanup
Session Reuse Patterns
Pattern 1: Simple Reuse
async function getBrowser(env: Env): Promise<Browser> {
// Try to connect to existing session
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);
if (freeSession) {
try {
return await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
} catch {
// Session closed, launch new one
}
}
// Launch new browser
return await puppeteer.launch(env.MYBROWSER);
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
const browser = await getBrowser(env);
// Do work
const page = await browser.newPage();
// ...
// Disconnect (keep alive)
await browser.disconnect();
return response;
}
};
Pattern 2: Reuse with Limits Check
async function getBrowserSafe(env: Env): Promise<Browser> {
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);
if (freeSession) {
try {
return await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
} catch {
// Continue to launch
}
}
// Check limits before launching
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
throw new Error(
`Rate limit reached. Retry after ${limits.timeUntilNextAllowedBrowserAcquisition}ms`
);
}
return await puppeteer.launch(env.MYBROWSER);
}
Pattern 3: Retry with Backoff
async function getBrowserWithRetry(
env: Env,
maxRetries = 3
): Promise<Browser> {
for (let i = 0; i < maxRetries; i++) {
try {
// Try existing session first
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);
if (freeSession) {
try {
return await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
} catch {
// Continue to launch
}
}
// Check limits
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions > 0) {
return await puppeteer.launch(env.MYBROWSER);
}
// Rate limited, wait and retry
if (i < maxRetries - 1) {
const delay = Math.min(
limits.timeUntilNextAllowedBrowserAcquisition,
Math.pow(2, i) * 1000 // Exponential backoff
);
await new Promise(resolve => setTimeout(resolve, delay));
}
} catch (error) {
if (i === maxRetries - 1) throw error;
}
}
throw new Error("Failed to acquire browser after retries");
}
Browser Timeout Management
Default Timeout
Browsers close after 60 seconds of inactivity (no devtools commands).
Inactivity means:
- No
page.goto() - No
page.screenshot() - No
page.evaluate() - No other browser/page operations
Extending Timeout with keep_alive
const browser = await puppeteer.launch(env.MYBROWSER, {
keep_alive: 300000 // 5 minutes = 300,000 ms
});
Maximum: 600,000ms (10 minutes)
Use Cases:
- Long-running scraping workflows
- Multi-step form automation
- Session reuse across multiple requests
Cost Impact:
- Longer keep_alive = more browser hours billed
- Only extend if actually needed
Incognito Browser Contexts
Use browser contexts to isolate cookies/cache while sharing a browser.
Benefits:
- 1 concurrent browser instead of N
- Separate cookies/cache per context
- Test multi-user scenarios
- Session isolation
Example:
const browser = await puppeteer.launch(env.MYBROWSER);
// Create isolated contexts
const context1 = await browser.createBrowserContext();
const context2 = await browser.createBrowserContext();
// Each context has separate state
const page1 = await context1.newPage();
const page2 = await context2.newPage();
await page1.goto("https://app.example.com"); // User 1
await page2.goto("https://app.example.com"); // User 2
// page1 and page2 have separate cookies
await context1.close();
await context2.close();
await browser.close();
Multiple Tabs vs Multiple Browsers
❌ Bad: Multiple Browsers
// Uses 10 concurrent browsers!
for (const url of urls) {
const browser = await puppeteer.launch(env.MYBROWSER);
const page = await browser.newPage();
await page.goto(url);
await browser.close();
}
Problems:
- 10x concurrency usage
- 10x cold start delays
- May hit concurrency limits
✅ Good: Multiple Tabs
// Uses 1 concurrent browser
const browser = await puppeteer.launch(env.MYBROWSER);
const results = await Promise.all(
urls.map(async (url) => {
const page = await browser.newPage();
await page.goto(url);
const data = await page.evaluate(() => ({
title: document.title
}));
await page.close();
return data;
})
);
await browser.close();
Benefits:
- 1 concurrent browser (10x reduction)
- Faster (no repeated cold starts)
- Cheaper (reduced concurrency charges)
Monitoring and Debugging
Log Session Activity
const browser = await puppeteer.launch(env.MYBROWSER);
const sessionId = browser.sessionId();
console.log({
event: "browser_launched",
sessionId,
timestamp: Date.now()
});
// Do work
await browser.disconnect();
console.log({
event: "browser_disconnected",
sessionId,
timestamp: Date.now()
});
Track Session Metrics
interface SessionMetrics {
sessionId: string;
launched: boolean; // true = new, false = reused
duration: number; // ms
operations: number; // page navigations
}
async function trackSession(env: Env, fn: (browser: Browser) => Promise<void>) {
const start = Date.now();
const sessions = await puppeteer.sessions(env.MYBROWSER);
const freeSession = sessions.find(s => !s.connectionId);
let browser: Browser;
let launched: boolean;
if (freeSession) {
browser = await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
launched = false;
} else {
browser = await puppeteer.launch(env.MYBROWSER);
launched = true;
}
await fn(browser);
const metrics: SessionMetrics = {
sessionId: browser.sessionId(),
launched,
duration: Date.now() - start,
operations: 1 // Track actual operations in production
};
await browser.disconnect();
return metrics;
}
Production Best Practices
-
Always Check Limits
- Call
puppeteer.limits()before launching - Handle rate limit errors gracefully
- Implement retry with backoff
- Call
-
Prefer Session Reuse
- Try
puppeteer.connect()first - Fall back to
puppeteer.launch()only if needed - Use
browser.disconnect()instead ofbrowser.close()
- Try
-
Use Multiple Tabs
- One browser, many tabs
- Reduces concurrency usage 10-50x
- Faster than multiple browsers
-
Set Appropriate Timeouts
- Default 60s is fine for most use cases
- Extend only if actually needed (keep_alive)
- Remember: longer timeout = more billable hours
-
Handle Errors
- Always
browser.close()on errors - Wrap
puppeteer.connect()in try-catch - Gracefully handle rate limits
- Always
-
Monitor Usage
- Log session IDs
- Track reuse rate
- Monitor concurrency in dashboard
-
Use Incognito Contexts
- Isolate sessions while sharing browser
- Better than multiple browsers
- Test multi-user scenarios safely
Cost Optimization
Scenario: Screenshot Service (1000 requests/hour)
Bad Approach (No Session Reuse):
- Launch new browser for each request
- 1000 browsers/hour
- Average session: 5 seconds
- Browser hours: (1000 * 5) / 3600 = 1.39 hours
- Average concurrency: ~14 browsers
- Cost: 1.39 hours = $0.13 + (14-10) * $2 = $8.13/hour
Good Approach (Session Reuse):
- Maintain pool of 3-5 warm browsers
- Reuse sessions across requests
- Average session: 1 hour (keep_alive)
- Browser hours: 5 hours (5 browsers * 1 hour)
- Average concurrency: 5 browsers
- Cost: 5 hours = $0.45/hour
Savings: 94% ($8.13 → $0.45)
Common Issues
Issue: "Failed to connect to session"
Cause: Session closed between .sessions() and .connect() calls
Solution:
const freeSession = sessions.find(s => !s.connectionId);
if (freeSession) {
try {
return await puppeteer.connect(env.MYBROWSER, freeSession.sessionId);
} catch (error) {
console.log("Session closed, launching new browser");
return await puppeteer.launch(env.MYBROWSER);
}
}
Issue: Sessions timing out too quickly
Cause: Default 60s idle timeout
Solution: Extend with keep_alive:
const browser = await puppeteer.launch(env.MYBROWSER, {
keep_alive: 300000 // 5 minutes
});
Issue: Rate limit reached
Cause: Too many concurrent browsers or launches per minute
Solution: Check limits before launching:
const limits = await puppeteer.limits(env.MYBROWSER);
if (limits.allowedBrowserAcquisitions === 0) {
return new Response("Rate limit reached", { status: 429 });
}
Reference
- Official Docs: https://developers.cloudflare.com/browser-rendering/workers-bindings/reuse-sessions/
- Limits: https://developers.cloudflare.com/browser-rendering/platform/limits/
- Pricing: https://developers.cloudflare.com/browser-rendering/platform/pricing/
Last Updated: 2025-10-22