Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer saves a screenshot only when you pass a path to page.screenshot() (or an element’s screenshot() method). A relative path is resolved from Node.js’s current working directory, not from a Puppeteer-specific screenshots folder. If you omit path, Puppeteer returns image data in memory instead of creating a file.

The rule that determines the destination

The path option is the complete destination filename. For example:

await page.screenshot({ path: 'screenshots/home.png' });

If the process was started in /work/app, that relative path points to /work/app/screenshots/home.png. The base is the value returned by process.cwd() at runtime. It can differ from the directory containing your JavaScript file when you launch the script from another directory, a test runner, an IDE, a Docker container, or a process manager.

Puppeteer does not silently choose a default directory or filename. The documented behavior is:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • A relative path is resolved against the current working directory.
  • No path means no disk write; the screenshot is returned to your code.
  • The filename extension is used to infer the image format when you do not provide type.

Relative paths: convenient, but dependent on launch location

Relative paths are useful for a small script or a project whose working directory is fixed:

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'screenshots/example.png' });
await browser.close();

Run this from the project directory and the output will be under that directory. Run it from elsewhere, and the same string points elsewhere. To see the directory before capturing, log it:

console.log('working directory:', process.cwd());

A common “missing screenshot” report is therefore a successful capture saved in a different directory than the developer expected. Search from the printed working directory, or use an absolute destination.

Absolute paths: deterministic output

An absolute path removes ambiguity caused by the launch directory. Build it with Node’s path utilities and create the parent directory before calling Puppeteer:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
import fs from 'node:fs/promises';
import path from 'node:path';
import puppeteer from 'puppeteer';

const outputDir = path.resolve(process.cwd(), 'artifacts');
await fs.mkdir(outputDir, { recursive: true });
const output = path.join(outputDir, 'home.png');

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: output });
  console.log(`Saved screenshot to ${output}`);
} finally {
  await browser.close();
}

path.resolve() turns the base into an absolute location, while fs.mkdir(..., { recursive: true }) makes missing parent directories. The screenshot path option identifies the file; do not rely on it to create a directory tree for you.

You can anchor output to the module location instead of the launch directory when that is what your deployment needs. In an ES module, convert import.meta.url to a filesystem path, then join your artifact directory. In CommonJS, __dirname provides the module directory. Choose one anchor deliberately: a working-directory-based path is often better for CI artifacts, whereas a module-relative path can be better for an application’s bundled assets.

Rank #2
Sale
HTML and CSS: Design and Build Websites
  • HTML CSS Design and Build Web Sites
  • Comes with secure packaging
  • It can be a gift option

What is saved, and what stays in memory?

Call pattern Destination Result Useful when
page.screenshot({ path: 'shots/a.png' }) Relative file under process.cwd() Image is written to disk The launch directory is controlled
page.screenshot({ path: '/var/tmp/a.png' }) Exact absolute filename Image is written to disk Workers, containers, and CI need a stable location
const bytes = await page.screenshot() No file A Uint8Array is returned Uploading directly to object storage or processing in memory
await page.screenshot({ encoding: 'base64' }) No file unless you write one yourself A base64 string is returned Embedding in a JSON response or data URL

For an in-memory upload, pass the returned bytes to your storage SDK rather than adding a temporary path. If another API requires base64, request encoding: 'base64'; otherwise the normal return is binary data.

Choose the output scope before choosing the filename

Viewport screenshot

The default captures the currently visible viewport:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.screenshot({ path: 'artifacts/viewport.png' });

The path controls where the file goes; viewport dimensions come from the page or from an explicit viewport set with page.setViewportSize-equivalent configuration during page setup.

Full-page screenshot

Set fullPage: true when the image should include the document beyond the visible viewport. The default is false:

await page.screenshot({
  path: 'artifacts/full-page.png',
  fullPage: true
});

This changes the captured area, not the destination. Very tall pages can require substantially more memory and encoding time than a viewport capture. If a page loads content only after scrolling, wait for the relevant content or trigger the page’s loading behavior before capturing.

One element

Use an element handle when you need a card, chart, or other component rather than the whole page:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
const card = await page.waitForSelector('.card');
if (!card) throw new Error('The .card element was not found');
await card.screenshot({ path: 'artifacts/card.png' });

ElementHandle.screenshot() attempts to scroll a hidden element into view before capturing it. A missing selector still fails your script, so wait for a selector that identifies the final rendered element and handle a null result explicitly.

Format and image controls

When path is present, Puppeteer infers the image type from the filename extension. Use a matching extension such as .png, .jpeg, or .webp. If the format must not depend on a filename, set type explicitly:

await page.screenshot({
  path: 'artifacts/hero-image',
  type: 'webp',
  quality: 82
});

Quality applies to lossy formats such as JPEG and WebP. Other documented screenshot controls include:

  • clip for a rectangular region of the page.
  • omitBackground: true to preserve transparency where the page and format support it.
  • captureBeyondViewport to control capture outside the current viewport for supported capture modes.
  • encoding to choose binary output or base64 when you are not writing directly to a path.

Keep the extension and type consistent. A mismatch can confuse downstream image viewers and pipelines even when the capture itself succeeds.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

A complete reusable capture function

This example makes the destination explicit, creates its directory, waits for the page, and always closes the browser:

import fs from 'node:fs/promises';
import path from 'node:path';
import puppeteer from 'puppeteer';

export async function capture(url, filename = 'page.png') {
  const directory = path.resolve(process.cwd(), 'artifacts');
  await fs.mkdir(directory, { recursive: true });
  const destination = path.join(directory, filename);

  const browser = await puppeteer.launch();
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900 });
    await page.goto(url, { waitUntil: 'networkidle2', timeout: 30_000 });
    await page.screenshot({ path: destination, fullPage: true });
    return destination;
  } finally {
    await browser.close();
  }
}

const savedAt = await capture('https://example.com', 'example-full.png');
console.log(savedAt);

Use a unique filename when several workers capture at once. A timestamp, job ID, or URL hash prevents one job from overwriting another. Keep the artifact directory outside temporary browser profiles if you need to collect it after the process exits.

Rank #4
Sale
Web Design with HTML, CSS, JavaScript and jQuery Set
  • Brand: Wiley
  • Set of 2 Volumes
  • A handy two-book set that uniquely combines related technologies Highly visual format and accessible language makes these books highly effective learning tools Perfect for beginning web designers and front-end developers

Troubleshooting: why the file is missing or unusable

The file exists, but not where expected

Cause: the path was relative and the process started in another directory. Fix: print process.cwd(), inspect that directory, or switch to path.resolve() and log the final absolute filename.

ENOENT or a “no such file or directory” error

Cause: the parent directory does not exist. Fix: call fs.mkdir(parent, { recursive: true }) before screenshot(). Do not assume a repository’s empty folder or an ignored CI directory will be present in a fresh checkout.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

EACCES, permission denied, or read-only filesystem

Cause: the Node process cannot write to the selected location, which is common in restricted containers and serverless runtimes. Fix: choose a writable temporary or mounted artifact directory, verify ownership and permissions, and use the returned bytes for an upload when local persistence is not available.

page.screenshot() returned bytes instead of a PNG file

Cause: no path was supplied. Fix: add a destination, or intentionally keep the bytes in memory and upload them. A screenshot call without path is not a failed save; it is the in-memory API.

The image is only the visible portion

Cause: fullPage defaults to false. Fix: set fullPage: true, or use an element screenshot when a single component is the required scope.

The element capture fails or is blank

Cause: the selector did not match, the element was rendered later, or its content depends on a state that was not reached. Fix: wait for a specific selector, wait for the relevant network or application state, verify the element’s dimensions, and capture only after it is visible and populated.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The format is wrong

Cause: the extension inferred a different type than your pipeline expects, or the explicit type conflicts with the filename. Fix: use a matching extension and, when necessary, set both type and quality deliberately.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Reliability and performance choices

  • Reuse a browser for a batch of pages, but create a fresh page per capture so cookies, viewport state, and DOM changes do not leak between jobs.
  • Use an explicit navigation timeout and a wait condition appropriate to the site. “Network idle” is not a guarantee that client-rendered data or animations have finished.
  • Prefer element or clipped captures when a full document is unnecessary; they reduce image dimensions and encoding work.
  • For full-page captures, test unusually long documents and pages with fixed-position headers. Large images consume memory both in Chromium and in Node while they are encoded or uploaded.
  • Write to a unique temporary filename and rename it after a successful capture if consumers must never see a partial file.
  • Record the resolved path, URL, viewport, format, and capture error in job logs. That makes a path problem distinguishable from a navigation or rendering problem.

Or skip the browser setup

If you only need a clean website image or PDF, ScreenshotNeo is a hosted alternative to maintaining Chromium, writable directories, and navigation waits. It accepts one GET request and returns a PNG, JPEG, WebP, or PDF. Before capture it accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off.

Only clean shots are billed. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server for Claude, Cursor, and other MCP clients with take_screenshot, get_page_info, and capture_pdf tools.

See the ScreenshotNeo API documentation for the complete parameter list. A direct cURL request is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The equivalent Python request is:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

In 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}`);
if (!res.ok) throw new Error(`ScreenshotNeo returned ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', data));

Beyond the URL and output format, the service exposes 63 options, including full-page capture with lazy images loaded, CSS-selector element capture, dark mode, 12 device presets and arbitrary viewports, retina scale, PDF paper size/margins/landscape/page ranges, HTML/CSS-to-image, custom CSS and JavaScript, pre-capture clicks, hidden selectors, waits for a selector or delay or network idle, blocking ads/trackers/requests/resource types, custom headers/cookies/user agent/Authorization, timezone and geolocation, transparent backgrounds, image resizing, selectable cache TTLs, signed links for public <img> tags, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, a usage API, and an OpenAPI specification. Parameter names used by other screenshot APIs also work, which can simplify migration. Every feature is included on every plan.

Plan Included shots Price
Free 1,000 per month $0, no card
Starter 3,000 $5
Growth 15,000 $15
Pro 60,000 $39
Scale 250,000 $99
Business 1,000,000 $249

Yearly billing gives two months free. You can start with 1,000 free screenshots a month with no card, then move to paid plans starting at $5 for 3,000 shots when a hosted capture endpoint fits your workflow.

Frequently Asked Questions

Does setting fullPage change where Puppeteer writes the image?

No. It changes the captured area only. The path value still determines the filename and directory.

Can I force a format when the filename has no extension?

Yes. Pass an explicit type such as 'webp' (and a suitable quality value for lossy formats) instead of relying on extension inference.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.