Puppeteer Screenshot to Base64: Complete JavaScript Guide
Convert Puppeteer screenshots to Base64 strings, data URIs, files, and API payloads with complete options, troubleshooting, performance guidance, and a hosted alternative.

To return a Puppeteer screenshot as a Base64 string, pass encoding: 'base64' to page.screenshot():
const base64 = await page.screenshot({ encoding: 'base64' });
The Base64 overload returns a JavaScript string. The normal screenshot overload returns binary image data. Puppeteer does not promise that the Base64 value includes a data:image/png;base64, prefix, so add that prefix yourself only when the receiving API expects a data URI. The official references are the Page.screenshot() API and ScreenshotOptions.
1. Install Puppeteer and capture a Base64 screenshot
Create a project and install Puppeteer:
mkdir puppeteer-base64
cd puppeteer-base64
npm init -y
npm install puppeteer
Save this as capture.mjs:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const base64 = await page.screenshot({
encoding: 'base64',
type: 'png'
});
console.log('Characters:', base64.length);
console.log(base64.slice(0, 40));
} finally {
await browser.close();
}
Run it with node capture.mjs. The browser launch, new page, navigation, screenshot, and close sequence follows Puppeteer’s documented Page API. Always close the browser in a finally block so a navigation or screenshot error does not leave a Chromium process running.
2. Base64 string versus binary bytes
Choose the output type based on the next system in your pipeline:
| Need | Use | Result |
|---|---|---|
| JSON, text transport, or an API field | encoding: 'base64' |
Base64 string |
| Write directly to disk | path: 'shot.png' |
File written by Puppeteer |
| Upload as bytes | Omit encoding |
Binary data (Uint8Array in current documentation) |
| Embed in HTML or CSS | Add a data-URI prefix | data:image/png;base64,... |
Base64 increases payload size by roughly one third compared with the original bytes. Keep the binary form when your storage or HTTP client accepts bytes. Use Base64 when the protocol is text-only or the image must be carried inside JSON.
3. Create a data URI safely
A raw Base64 value is not automatically a browser data URI. Build the prefix from the actual image type:
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
const type = 'png';
const base64 = await page.screenshot({ encoding: 'base64', type });
const dataUri = `data:image/${type};base64,${base64}`;
console.log(dataUri);
} finally {
await browser.close();
}
For JPEG use data:image/jpeg;base64,; for WebP use data:image/webp;base64,. Do not infer the MIME type from a filename if you selected a different type option.
4. Screenshot options that affect Base64 output
encoding
encoding accepts 'base64' or 'binary'. The documented default is 'binary'. Set it explicitly whenever a function contract requires a string. Do not rely on a default changing between Puppeteer versions.
type and quality
The default image type is PNG. PNG is lossless and supports transparency, but it can be large. JPEG usually produces a smaller payload for photographs and does not support transparency. WebP can reduce size when your consumer supports it. The quality setting applies to JPEG and WebP; it does not apply to PNG.
const jpeg = await page.screenshot({
encoding: 'base64',
type: 'jpeg',
quality: 80
});
const webp = await page.screenshot({
encoding: 'base64',
type: 'webp',
quality: 80
});
fullPage
Use fullPage: true to capture the full scrollable page instead of the current viewport:
const fullPageBase64 = await page.screenshot({
encoding: 'base64',
fullPage: true,
type: 'png'
});
Full-page images can be very tall. Memory use, Base64 length, and downstream upload time all increase with pixel dimensions. Set the viewport before navigation when consistent output matters:
await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });
path
path is a separate output choice. Puppeteer can write the screenshot to a file, but do not expect path to turn the return value into Base64. If you need both, capture bytes once and encode them yourself, or perform the two output operations deliberately:
import { writeFile } from 'node:fs/promises';
const bytes = await page.screenshot({ type: 'png' });
await writeFile('shot.png', bytes);
const base64 = Buffer.from(bytes).toString('base64');
The Page class reference documents the path-based screenshot flow. Encoding bytes yourself is useful when you need a Buffer for hashing, upload, and Base64 conversion in one process.
5. Capture an element as Base64
For a specific component, locate an element and call its screenshot method:

const card = await page.$('.pricing-card');
if (!card) throw new Error('pricing card not found');
const base64 = await card.screenshot({
encoding: 'base64',
type: 'png'
});
Puppeteer scrolls the element into view when needed. The documented ElementHandle.screenshot() method throws if the handle has been detached from the DOM. Dynamic frameworks can replace nodes during rendering, so query the element as late as practical and wait for a stable selector.
await page.waitForSelector('.pricing-card', { visible: true });
const card = await page.$('.pricing-card');
if (!card) throw new Error('pricing card disappeared');
const base64 = await card.screenshot({ encoding: 'base64' });
6. Wait for the page before capturing
A screenshot captures the rendered state at the moment the call runs. Pick a navigation and readiness strategy that matches the site:
waitUntil: 'domcontentloaded'waits for the initial HTML.waitUntil: 'load'also waits for load events.waitUntil: 'networkidle2'waits for a period with at most two active connections.waitForSelector()is best when a particular component proves that rendering finished.waitForTimeout()can cover a known animation, but fixed delays are less reliable than a state-based wait.
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
timeout: 45_000
});
await page.waitForSelector('#report', { visible: true, timeout: 20_000 });
await page.evaluate(() => document.fonts?.ready);
const base64 = await page.screenshot({ encoding: 'base64', fullPage: true });
Pages with analytics, polling, websockets, or advertisements may never become truly idle. In those cases, use a selector or a bounded delay and keep the navigation timeout finite.
7. Complete reusable helper
This helper validates the URL, configures the viewport, waits for a selector when supplied, and returns both the Base64 string and a data URI:
import puppeteer from 'puppeteer';
export async function screenshotToBase64(url, options = {}) {
const {
selector,
fullPage = false,
type = 'png',
quality,
width = 1280,
height = 800,
timeout = 45_000
} = options;
const parsed = new URL(url);
if (!['http:', 'https:'].includes(parsed.protocol)) {
throw new Error('Only HTTP and HTTPS URLs are supported');
}
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width, height, deviceScaleFactor: 1 });
await page.goto(parsed.href, { waitUntil: 'domcontentloaded', timeout });
if (selector) {
await page.waitForSelector(selector, { visible: true, timeout });
}
const shotOptions = { encoding: 'base64', type, fullPage };
if (quality !== undefined && type !== 'png') shotOptions.quality = quality;
const base64 = selector
? await (await page.$(selector)).screenshot(shotOptions)
: await page.screenshot(shotOptions);
return {
base64,
dataUri: `data:image/${type};base64,${base64}`,
mimeType: `image/${type}`
};
} finally {
await browser.close();
}
}
const result = await screenshotToBase64('https://example.com', {
fullPage: true,
type: 'webp',
quality: 82
});
console.log(result.dataUri.slice(0, 60));
In production, add concurrency limits around browser pages, validate allowed destinations, and avoid logging full image strings because they can be large and may contain sensitive page content.
8. Send Base64 to another service
Put the string in JSON only when the receiving service documents that format:
const response = await fetch('https://api.example.test/images', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
mime_type: 'image/png',
image_base64: base64
})
});
if (!response.ok) throw new Error(`Upload failed: ${response.status}`);
For a multipart or binary endpoint, send a Buffer instead. Base64 inside JSON can hit request-size limits sooner than a binary upload.
9. cURL, Python, and hosted API alternatives
If your service only needs a screenshot response and you do not want to operate Chromium, ScreenshotNeo provides a single HTTP request. See the ScreenshotNeo documentation for the current API details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
To turn the Node response into Base64, read the bytes and encode them:
if (!res.ok) throw new Error(`ScreenshotNeo request failed: ${res.status}`);
const bytes = Buffer.from(await res.arrayBuffer());
const base64 = bytes.toString('base64');
10. Or skip the browser setup
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots each month without a card; paid plans start at $5 for 3,000 shots. Every plan includes the available features, including full-page and element capture, custom CSS and JavaScript, waits, blocking controls, device settings, PDFs, caching, signed links, asynchronous jobs, bulk capture, usage data, and an OpenAPI specification.

Create a free ScreenshotNeo account and start with the 1,000 monthly shots.
11. Troubleshooting common errors
| Symptom | Likely cause | Fix |
|---|---|---|
| Return value is not a string | encoding was omitted |
Set encoding: 'base64', or convert returned bytes with Buffer.from(bytes).toString('base64'). |
| Image does not render from the string | Missing or incorrect data-URI prefix | Prepend the MIME type that matches type. |
| Quality has no effect | PNG was selected | Use JPEG or WebP when lossy compression is acceptable. |
TimeoutError on navigation |
Slow resources, polling, or a blocked page | Set a justified timeout, use domcontentloaded, and wait for a specific selector. |
| Blank or incomplete screenshot | Capture ran before client-side rendering or fonts loaded | Wait for a visible selector and document.fonts.ready; handle lazy content explicitly. |
| Element handle detached | Framework replaced the DOM node | Wait, query the selector again, and capture the fresh handle. |
| Chromium fails to launch in a container | Sandbox or missing system dependencies | Install Puppeteer’s required browser dependencies and follow your container’s security policy; avoid disabling sandbox protections unless your environment requires it. |
| Upload rejected for size | Base64 expansion or full-page dimensions | Use WebP/JPEG, reduce viewport or scope to an element, or upload binary bytes. |
12. Performance, reliability, and cost considerations
Performance
- Reuse a browser process and create pages per job instead of launching Chromium for every screenshot.
- Limit concurrent pages according to available CPU and memory.
- Prefer element captures or viewport captures when a full page is unnecessary.
- Choose WebP or JPEG for smaller transfer sizes, and set a quality value that preserves the details your consumer needs.
- Avoid converting bytes to Base64 until the final boundary that requires text.
Reliability
- Use finite navigation and selector timeouts.
- Close pages and browsers in cleanup paths.
- Retry transient navigation failures with a limit and backoff, while treating authentication and invalid URLs as permanent errors.
- Record URL, viewport, image type, duration, and failure category without logging the image itself.
- Keep screenshot generation isolated from untrusted navigation targets when your application accepts user URLs.
Cost
Self-hosted Puppeteer costs whatever your compute, browser maintenance, storage, and engineering time require. Base64 also consumes additional bandwidth and memory. A hosted API can reduce browser operations work; compare its per-shot price and controls with your volume and latency requirements. ScreenshotNeo’s listed plans are Free (1,000/month), Starter $5 (3,000), Growth $15 (15,000), Pro $39 (60,000), Scale $99 (250,000), and Business $249 (1,000,000); yearly billing provides two months free.
13. FAQ
Does Puppeteer return a data URI?
No. With encoding: 'base64' it returns the Base64 content. Add the correct data:image/...;base64, prefix when required.
Can I use Base64 with fullPage?
Yes. Combine fullPage: true and encoding: 'base64', while accounting for the larger memory and payload size.
Should I use PNG, JPEG, or WebP?
Use PNG for lossless graphics and transparency. Use JPEG or WebP when smaller files matter and lossy compression is acceptable.
Can an element screenshot be Base64?
Yes. Call elementHandle.screenshot({ encoding: 'base64' }) after ensuring the handle is still attached.
What if the next API accepts bytes only?
Omit Base64 encoding and pass the returned bytes, or convert the Base64 string back into a Buffer before uploading.


