# 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:** ```typescript await puppeteer.sessions(binding: Fetcher): Promise ``` **Response:** ```typescript interface SessionInfo { sessionId: string; // Unique session ID startTime: number; // Unix timestamp (ms) connectionId?: string; // Present if worker is connected connectionStartTime?: number; } ``` **Example:** ```typescript 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:** ```json [ { "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:** ```typescript await puppeteer.history(binding: Fetcher): Promise ``` **Response:** ```typescript 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:** ```typescript 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:** ```typescript await puppeteer.limits(binding: Fetcher): Promise ``` **Response:** ```typescript interface LimitsInfo { activeSessions: Array<{ id: string }>; maxConcurrentSessions: number; allowedBrowserAcquisitions: number; // Can launch now? timeUntilNextAllowedBrowserAcquisition: number; // ms to wait } ``` **Example:** ```typescript const limits = await puppeteer.limits(env.MYBROWSER); console.log({ active: limits.activeSessions.length, max: limits.maxConcurrentSessions, canLaunch: limits.allowedBrowserAcquisitions > 0, waitTime: limits.timeUntilNextAllowedBrowserAcquisition }); ``` **Output:** ```json { "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:** ```typescript await puppeteer.connect(binding: Fetcher, sessionId: string): Promise ``` **Example:** ```typescript 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:** ```typescript browser.sessionId(): string ``` **Example:** ```typescript 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:** ```typescript await browser.disconnect(): Promise ``` **When to use:** - Want to reuse session later - Keep browser warm for next request - Reduce cold start times **Example:** ```typescript 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_alive` to 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:** ```typescript await browser.close(): Promise ``` **When to use:** - Done with browser completely - Want to free concurrency slot - Error occurred during processing **Example:** ```typescript 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 ```typescript async function getBrowser(env: Env): Promise { // 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 { 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 ```typescript async function getBrowserSafe(env: Env): Promise { 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 ```typescript async function getBrowserWithRetry( env: Env, maxRetries = 3 ): Promise { 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 ```typescript 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:** ```typescript 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 ```typescript // 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 ```typescript // 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 ```typescript 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 ```typescript 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) { 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 1. **Always Check Limits** - Call `puppeteer.limits()` before launching - Handle rate limit errors gracefully - Implement retry with backoff 2. **Prefer Session Reuse** - Try `puppeteer.connect()` first - Fall back to `puppeteer.launch()` only if needed - Use `browser.disconnect()` instead of `browser.close()` 3. **Use Multiple Tabs** - One browser, many tabs - Reduces concurrency usage 10-50x - Faster than multiple browsers 4. **Set Appropriate Timeouts** - Default 60s is fine for most use cases - Extend only if actually needed (keep_alive) - Remember: longer timeout = more billable hours 5. **Handle Errors** - Always `browser.close()` on errors - Wrap `puppeteer.connect()` in try-catch - Gracefully handle rate limits 6. **Monitor Usage** - Log session IDs - Track reuse rate - Monitor concurrency in dashboard 7. **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:** ```typescript 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: ```typescript 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: ```typescript 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