How to Persist Cookie Consent Between Puppeteer Screenshot Runs
Keep consent between Puppeteer screenshots by reusing a browser profile or restoring the site’s actual consent state into a fresh context.
To keep cookie consent between separate Puppeteer screenshot runs, either launch each run with the same persistent userDataDir, or save the consent state and restore it into the next browser context before navigating to the site. Reusing a profile is usually the simplest option when runs should represent the same returning browser. Explicit restoration is better when you need isolated contexts or want to seed only selected state.
First determine how the target site stores consent. A consent manager may use cookies, localStorage, or another mechanism. There is no universal cookie name or storage key. Puppeteer browser contexts isolate cookies and localStorage, so creating a new context does not automatically inherit consent from the previous one. Puppeteer BrowserContext documentation
Choose a persistence method
| Method | Use it when | Trade-off |
|---|---|---|
Reuse userDataDir |
Separate launches should behave like one returning browser profile. | Reuses broader profile state, not just consent. Keep the profile private and avoid concurrent launches against the same directory. |
| Save and restore cookies | You create fresh contexts and the site’s consent is represented by cookies. | You must preserve cookie scope and relevant attributes, and handle expiry. |
| Save and restore other storage | Inspection shows consent is stored in localStorage or another site-specific mechanism. | The storage key and format depend on that site; implement and validate them specifically. |
Option 1: Reuse a browser profile across launches
Puppeteer’s launch option userDataDir sets the path to the browser user data directory. Use the same stable path on each run. The browser can then reuse the profile’s stored site state, including consent where the site stores it in that profile. Puppeteer LaunchOptions documentation
import puppeteer from 'puppeteer';
const profileDir = './puppeteer-profile';
const url = 'https://example.com';
const browser = await puppeteer.launch({
headless: true,
userDataDir: profileDir,
});
try {
const page = await browser.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Run this script again with the same profileDir. After accepting the site’s consent banner once, subsequent launches can use the state kept in that profile, subject to the site’s own expiry and consent rules.
Profile reuse checklist
- Use a stable, writable directory path. A relative path is resolved from the process working directory; an absolute path can make deployment behavior clearer.
- Accept consent on the target site using the same profile directory.
- Close the browser cleanly after each run so the profile can be reopened.
- Do not run multiple browser processes against the same profile directory at once; use separate profile directories for concurrent workers.
- Protect the profile directory. Browser profiles can contain cookies and other browsing data.
Option 2: Save and restore consent cookies
If each run must use a fresh context, persist the relevant cookie records and set them on the next context before visiting the target page. Puppeteer documents cookie read and write APIs for browser contexts; context-scoped methods keep the operation attached to the isolated context you are managing. Puppeteer cookie guide, BrowserContext.cookies(), BrowserContext.setCookie()
The example below writes cookies to a local JSON file after you accept consent. On a later run, it loads those records and sets them before navigation. Replace the URL with the same site where the cookies were collected. This is a runnable pattern for cookie-based consent; it cannot restore a site’s localStorage state.
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
const url = 'https://example.com';
const cookieFile = './consent-cookies.json';
const browser = await puppeteer.launch({ headless: true });
try {
const context = await browser.createBrowserContext();
const page = await context.newPage();
let savedCookies = [];
try {
savedCookies = JSON.parse(await fs.readFile(cookieFile, 'utf8'));
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
if (savedCookies.length) {
await context.setCookie(...savedCookies);
}
await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 60_000 });
// If consent has not yet been accepted, handle the site's consent UI here.
// The button selector is site-specific and must be inspected on the target site.
// Example only: await page.locator('button.accept-consent').click();
const cookies = await context.cookies(url);
await fs.writeFile(cookieFile, JSON.stringify(cookies, null, 2), { mode: 0o600 });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
For an initial run, the site must actually set its consent cookie before context.cookies(url) can collect it. Add the site-specific interaction or accept consent manually during setup, then save the resulting state. Avoid saving every cookie from a broad browsing session if only a small consent subset is needed.
Cookie attributes and expiry
Preserve the cookie records returned by Puppeteer rather than reducing them to name/value pairs. Domain and path determine where a cookie applies, and other attributes such as secure, httpOnly, sameSite, and expires can affect whether it is accepted and sent. Puppeteer documents an omitted expiry as a session cookie, so session-cookie behavior differs from persistent-cookie behavior. Puppeteer Cookie reference
Cookie APIs have evolved: page-level cookie methods are deprecated in favor of browser- or context-level methods. Prefer BrowserContext.cookies() and BrowserContext.setCookie() when handling isolated contexts. Puppeteer Page API
Option 3: Persist localStorage or other site state
When the consent manager stores its choice in localStorage, restoring cookies alone will not help. Browser contexts isolate localStorage along with cookies. Inspect the target page after consenting to learn which storage mechanism actually changes; do not assume a guessed key is universal. Puppeteer BrowserContext documentation
One practical pattern is to visit the origin, restore the values into that origin, and then navigate to the page you want to capture. Replace the example key and value with what inspection shows for the specific site:
import fs from 'node:fs/promises';
import puppeteer from 'puppeteer';
const origin = 'https://example.com';
const targetUrl = 'https://example.com/pricing';
const storageFile = './consent-local-storage.json';
const entries = JSON.parse(await fs.readFile(storageFile, 'utf8'));
const browser = await puppeteer.launch({ headless: true });
try {
const context = await browser.createBrowserContext();
const page = await context.newPage();
// localStorage is origin-scoped, so load the origin before writing values.
await page.goto(origin, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.evaluate((items) => {
for (const [key, value] of items) localStorage.setItem(key, value);
}, entries);
await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 60_000 });
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
The JSON file for this pattern should contain an array of key/value pairs, for example [["site-specific-consent-key","site-specific-value"]], using values observed on the actual site. Some sites may require more than localStorage, may validate a consent token server-side, or may update consent state during page scripts. Test the restored state by checking whether the banner remains and whether the site behaves as expected.
Capture sequencing and context behavior
Create the context, restore its state, and only then navigate to the page to capture. This gives the page’s initial load access to the stored consent. If the site’s own scripts require a delay or additional interaction to settle the banner, use a site-appropriate wait condition before taking the screenshot.
Page.screenshot() captures the current page. Puppeteer documents that BrowserContext.newPage(), Browser.newPage(), and Page.close() wait while a screenshot in the same context is in progress. That lifecycle synchronization does not persist consent; storage must still be saved or reused through one of the methods above. Puppeteer Page.screenshot()
Do-it-yourself troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The banner returns after every launch. | The script launches with a different or temporary profile directory, or consent is not stored in that profile. | Log and verify the resolved userDataDir path. Reuse it, or inspect the site’s storage and restore the right state. |
| The banner returns in a new BrowserContext. | Contexts isolate cookies and localStorage. | Seed the new context before navigation, or use the default context/profile when isolation is not required. |
| Restored cookies do not suppress the banner. | The site uses localStorage or another mechanism, the cookie is expired, or its domain/path does not match. | Inspect storage after accepting consent, preserve cookie attributes, and verify the target URL matches the cookie scope. |
setCookie() rejects a record. |
The serialized record may have incompatible or incomplete fields, or invalid expiry data. | Use the cookie objects returned by Puppeteer where possible. Check domain, path, expiry, and supported fields against the installed Puppeteer API. |
| The browser cannot open the profile. | The directory may not be writable, may be locked by another browser process, or may be unavailable in the runtime environment. | Choose a writable stable directory, ensure the previous process has closed, and give concurrent workers separate directories. |
| The banner flashes briefly in the screenshot. | The screenshot happens before the site finishes applying stored state or removing its banner. | Restore state before navigation and wait for a meaningful site-specific condition before capturing. |
| The cookie file contains sensitive data. | Cookies can grant access or reveal browsing state. | Restrict file permissions, keep it out of source control, and store only the necessary consent records. |
Performance, reliability, and cost
Reusing a profile avoids implementing a state export/import path, while cookie restoration gives each run an explicit setup step. These are workflow trade-offs, not measured performance claims. A shared profile can accumulate unrelated state; explicit restoration is easier to inspect but can become stale when consent expires or the site changes its storage format.
For reliable captures, make the storage choice explicit, restore it before navigation, use a stable profile or versioned state file, and treat consent expiry as normal. Keep independent profiles for parallel jobs. Puppeteer itself does not provide a universal consent state: the target site decides how consent is stored and when it remains valid.
Local Puppeteer runs have no per-screenshot API fee, but they require a browser runtime and your own compute, storage, maintenance, and handling of browser state. If a screenshot service fits the workflow better, compare its capture options and billing behavior before migrating.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. A single GET request returns an image or PDF. Its clean capture flow accepts consent banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing status. AI agents can use its MCP server tools, including take_screenshot, get_page_info, and capture_pdf. Every feature is on every plan: 1,000 shots per month are free with no card, and paid plans start at $5 for 3,000. See the ScreenshotNeo website and API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);
Start with a free ScreenshotNeo account: 1,000 screenshots a month, no card required.
FAQ
Does browser.newPage() reuse the previous page’s cookies?
Pages in the same browser context share that context’s storage. A new isolated context has separate cookies and localStorage, so it needs explicit state restoration.
Can I persist only one consent cookie while keeping other state isolated?
Yes. Read the relevant cookie from the accepted session and restore only that record into the new context, retaining its scope and attributes.
Will a saved consent cookie work forever?
No. Its expiry and the site’s consent policy determine how long it remains valid. A site may also revoke or replace its consent state.
Should I use a persistent profile for parallel screenshots?
Use separate profile directories for concurrent browser processes. A single shared profile is intended for sequential reuse, and its broader state may influence captures.


