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

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

Chrome does not let Puppeteer choose a download’s final filename through a documented high-level download API. A reliable approach is to configure Chrome to save into a directory your script controls, track the download through the Chrome DevTools Protocol (CDP), wait for it to finish, then rename the completed file with Node.js. Your script—not Chrome—must decide what to do when a name already exists.

How the download-and-rename workflow works

Puppeteer’s Files guide says, “Currently, Puppeteer does not offer a way to handle file downloads in a programmatic way.” That means there is no documented high-level Puppeteer download handler to await and rename for you. Use a CDP session to configure and observe Chrome’s download, then use Node.js filesystem operations to assign the final name. See the Puppeteer Files guide.

  1. Create an absolute download directory that belongs to the job.
  2. Launch Chrome and configure download behavior before triggering the download.
  3. Listen for CDP download events and correlate the download with its GUID.
  4. Wait for completion, locate the completed file, and verify it is no longer being written.
  5. Choose a final name using an explicit duplicate policy, then move the file.

This separation matters: Chrome’s suggested filename is not a guarantee of the exact on-disk name, and a completion event’s filePath may be missing or point to a file that does not exist. Treat CDP events as signals, then confirm the filesystem state. The protocol’s download controls are documented as experimental, so verify compatibility with the Chrome version used in your job.

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

Choose Chrome’s download naming policy

Approach What Chrome saves Best fit What your script still does
allowAndName A GUID-based name rather than the server’s human-readable suggested name. Jobs that need distinct temporary filenames, especially when downloads may overlap. Map the GUID to job context or the suggested name, then rename after verifying completion.
allow Chrome saves using its normal download naming behavior, which may include duplicate suffixes. Jobs that want Chrome’s suggested name as an input to the final naming policy. Inspect the directory, identify the resulting file, and apply your own collision policy.

Both policies require a download path. allowAndName helps avoid collisions at Chrome’s temporary-save stage, but it does not produce a custom descriptive filename by itself. The protocol and Puppeteer API describe the policy and path fields: Chrome DevTools Protocol: Browser.setDownloadBehavior and Puppeteer DownloadBehavior API.

#1 Best Overall
Panasonic Toughbook CF-31 MK5 Rugged Laptop, 13.1in i5, 8GB 256GB (Renewed)
  • [ULTRA-RUGGED DESIGN] MIL-STD-810G and IP65 certified. Built to survive 6-foot drops, heavy rain, and extreme vibrations. Features a magnesium alloy chassis with an integrated carry handle for maximum portability
  • [4G LTE - WORK ANYWHERE] Integrated 4G LTE Multi-Carrier Mobile Broadband. Stay connected to the internet in remote areas or on the road without relying on Wi-Fi or phone hotspots. True mobile freedom for field professionals
  • [1200-NIT SUNLIGHT READABLE] 13.1" XGA Touchscreen with CircuLumin technology. At 1200 nits, it is nearly 4x brighter than a standard laptop, ensuring perfect visibility under direct, intense sunlight
  • [LINUX UBUNTU PRE-INSTALLED] Fast, secure, and bloatware-free. Optimized for developers, network engineers, and diagnostic software that thrives in a stable, open-source environment
  • [LEGACY SERIAL PORT] Features a native RS-232 Serial Port, HDMI, and USB 3.0. Essential for connecting directly to industrial machinery, CNCs, and automotive diagnostic tools without unreliable adapter

Set up Puppeteer and Chrome on Ubuntu

Use a maintained Puppeteer installation and pin the Puppeteer and browser versions in repeatable automation. Puppeteer’s browser tooling documents installing Chrome dependencies on Debian or Ubuntu; installing system packages requires root privileges. The exact packages can depend on the Ubuntu release and browser build, so use the documented installer rather than copying an unrelated package list. See Puppeteer browsers API.

For an existing Puppeteer project, make sure its browser installation and runtime environment match the version you pin. If Chrome cannot start because libraries are absent, run the supported dependency installation step in an environment where you have the necessary system privileges, then rerun the job.

Complete Node.js example: download, detect completion, and rename

The following CommonJS example uses a dedicated download directory and allowAndName. It tracks the GUID from Browser.downloadWillBegin, waits for the matching Browser.downloadProgress event to report completion, then verifies that the corresponding file exists and has stable size before renaming it. It uses the reported filePath when available, but falls back to the GUID-named file in the controlled directory.

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

Install Puppeteer in the project with npm install puppeteer. Save the example as download.js, replace the example URL with a URL that actually initiates a file download, and run node download.js.

const fs = require('node:fs/promises');
const path = require('node:path');
const puppeteer = require('puppeteer');

const downloadUrl = 'https://example.com/report.pdf';
const downloadDir = path.resolve(__dirname, 'downloads');
const requestedName = 'report.pdf';
const timeoutMs = 120_000;

async function exists(file) {
  try {
    await fs.access(file);
    return true;
  } catch (error) {
    if (error.code === 'ENOENT') return false;
    throw error;
  }
}

function safeFilename(name) {
  const leaf = path.basename(name).replace(/[\/]/g, '_').trim();
  return leaf && leaf !== '.' && leaf !== '..' ? leaf : 'download';
}

async function uniqueDestination(dir, filename) {
  const ext = path.extname(filename);
  const stem = path.basename(filename, ext);
  let candidate = path.join(dir, filename);
  let index = 2;
  while (await exists(candidate)) {
    candidate = path.join(dir, `${stem}-${index}${ext}`);
    index += 1;
  }
  return candidate;
}

async function waitForStableFile(file, timeout = timeoutMs) {
  const deadline = Date.now() + timeout;
  let previousSize = -1;
  let stableChecks = 0;

  while (Date.now() < deadline) {
    try {
      const stat = await fs.stat(file);
      if (stat.isFile() && stat.size === previousSize) {
        stableChecks += 1;
        if (stableChecks >= 3) return;
      } else {
        previousSize = stat.size;
        stableChecks = 0;
      }
    } catch (error) {
      if (error.code !== 'ENOENT') throw error;
    }
    await new Promise(resolve => setTimeout(resolve, 300));
  }
  throw new Error(`Timed out waiting for a stable file: ${file}`);
}

(async () => {
  await fs.mkdir(downloadDir, { recursive: true });
  const browser = await puppeteer.launch({ headless: true });

  try {
    const page = await browser.newPage();
    const cdp = await page.createCDPSession();
    const downloadStarted = new Promise((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error('Timed out waiting for download start')), timeoutMs);
      cdp.on('Browser.downloadWillBegin', event => {
        clearTimeout(timer);
        resolve(event);
      });
    });

    await cdp.send('Browser.setDownloadBehavior', {
      behavior: 'allowAndName',
      downloadPath: downloadDir,
      eventsEnabled: true,
    });

    await page.goto(downloadUrl, { waitUntil: 'domcontentloaded', timeout: timeoutMs });
    const started = await downloadStarted;
    const completed = new Promise((resolve, reject) => {
      const timer = setTimeout(() => reject(new Error(`Timed out waiting for download ${started.guid}`)), timeoutMs);
      cdp.on('Browser.downloadProgress', event => {
        if (event.guid !== started.guid) return;
        if (event.state === 'completed') {
          clearTimeout(timer);
          resolve(event);
        } else if (event.state === 'canceled') {
          clearTimeout(timer);
          reject(new Error(`Chrome canceled download ${started.guid}`));
        }
      });
    });
    const finished = await completed;

    const candidates = [
      finished.filePath,
      path.join(downloadDir, started.guid),
    ].filter(Boolean);
    let source;
    for (const candidate of candidates) {
      if (await exists(candidate)) {
        source = candidate;
        break;
      }
    }
    if (!source) {
      throw new Error(`Download completed but no file was found. GUID: ${started.guid}; suggested name: ${started.suggestedFilename}`);
    }

    await waitForStableFile(source);
    const finalName = safeFilename(requestedName || started.suggestedFilename || 'download');
    const destination = await uniqueDestination(downloadDir, finalName);
    await fs.rename(source, destination);
    console.log(`Saved ${destination}`);
  } finally {
    await browser.close();
  }
})().catch(error => {
  console.error(error);
  process.exitCode = 1;
});

CDP event and command names are case-sensitive. This example observes events from the page’s CDP session; if your application can start several downloads from different targets or tabs, create and manage a session for the relevant target and correlate each event by GUID. The protocol’s suggestedFilename is useful metadata, not proof of the saved path.

Rank #2
HP 17 Business Laptop - Linux Mint Cinnamon - Intel Quad-Core i5-10210U, 32GB RAM, 1TB PCIe NVMe SSD + 1TB Storage HDD, 17.3" Inch HD+ (1600x900) Display
  • Intel Core i5-10210U (up to 4.2GHz) - 1TB PCIe NVMe + 1TB HDD - 32GB DDR4 SDRAM
  • 17.3" HD+ (1600x900) Display, Intel UHD Graphics 620
  • Built in HD 720p Webcam with Microphone - Bluetooth Version4.2
  • I/O Ports: 2x USB 3.1 (Data Only), 1x USB 2.0, 1x HDMI, 1x Headphone/Microphone Combo Jack
  • Linux Mint Cinnamon 64-Bit - 6-Row Keyboard w/ Full Numberpad

Adapt the trigger to your page

Navigating directly to a file URL triggers the download in the sample. For a button-driven download, navigate to the page containing that button and click it only after Browser.setDownloadBehavior has been sent. For example, replace the page.goto(downloadUrl, ...) line with:

await page.goto('https://example.com/reports', { waitUntil: 'domcontentloaded' });
await page.locator('a[data-report="monthly"]').click();

Use a selector that matches the site you automate. If the click opens a new tab, the download may belong to that target; attach the CDP session to the target that initiates the download and verify event delivery for the pinned Chrome version.

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

Set the duplicate rule you actually want

The example keeps the first report.pdf and then chooses report-2.pdf, report-3.pdf, and so on. For parallel workers, that check-then-rename sequence can race: two processes might both observe the same available destination. Prefer a job-specific directory, include a stable job identifier in the filename, or coordinate writers with an application-level lock. Do not assume one universal overwrite outcome across Node.js versions and filesystems; verify the behavior for your deployment.

Names originating from a server or user input should be sanitized before use. path.basename prevents a remote name from selecting a different directory, while the replacement step removes path separators and NUL characters. If the extension is important, preserve it when adding a duplicate suffix, as the example does.

cURL, Python, and Node.js API alternative

If your task is to capture a webpage as an image or PDF rather than automate an application’s download flow, a screenshot API avoids managing a local Chrome download directory. ScreenshotNeo is a website screenshot API and MCP server; it can return a screenshot or PDF from a URL. For this Puppeteer download task, it is an alternative only when the desired output is a page capture, not a file that the target site serves for download.

Rank #3
Lenovo IdeaPad Slim 3 Linux Laptop, 15.6" FHD Touchscreen Laptop, 8-Core AMD Ryzen 7 5825U, 16GB RAM, 512GB SSD, Keypad, SD Card Reader, Stylus Pen + External Portable SSD + USB Hub, Linux Ubuntu OS
  • Powerful Linux Laptop: This IdeaPad Slim 3 Laptop comes pre-installed with Ubuntu Linux, offering fast performance, robust security, and a clean, user-friendly experience. Enjoy full customization, seamless hardware compatibility, and access to thousands of open-source apps. Whether you're working, creating, or coding, it's built to keep up with everything you do.
  • A Multitasking Master: The latest AMD Ryzen 7 5825U processor (up to 4.5 GHz) delivers powerful performance with 8 cores and 16 threads for smooth multitasking. Integrated AMD Radeon Graphics provide crisp visuals for streaming, browsing, photo editing, and casual gaming. With smart machine intelligence, it adapts to your needs for a fast, responsive experience.
  • 15.6" Full HD Display: The IdeaPad Slim 3 boasts an 88% screen-to-body ratio for a floating, edge-to-edge visual experience. TÜV Low Blue Light certification reduces eye strain, making it perfect for long work or study sessions.
  • Military-Grade Durability: The smart IdeaPad Slim 3 combines portability and durability, letting you work, study, and play on the go. With a profile 10% slimmer than the previous generation, it's lightweight yet military-grade rugged, ready for anything, anywhere.
  • Versatile Connectivity: Enjoy the security of a built-in webcam with a privacy shutter. Connect effortlessly with multiple ports: 2x USB A, 1x USB C, 1x HDMI, 1x SD Card Reader, 1x Headphone/Microphone combo. Bundle comes with Stylus Pen, 256GB Portable SSD and 5-in-1 Docking Station.

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}`);

See the ScreenshotNeo API documentation for request options. ScreenshotNeo removes cookie/consent banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, and failed loads are not billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for ScreenshotNeo’s free plan.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Troubleshooting

No download-start event arrives

  • Confirm that the page action actually starts a download; a click may instead navigate, open a new tab, or be blocked by the site.
  • Send Browser.setDownloadBehavior before the click or navigation that triggers the file.
  • Check that the CDP session is attached to the target initiating the download and that eventsEnabled is true.
  • Allow enough time for the site to finish authentication, redirect, or prepare the file before expecting a download.

The download times out or is canceled

  • Increase the timeout for large or slow files, and distinguish a canceled event from a timeout in logs.
  • Check whether the page requires a signed-in session, cookies, or a valid authorization flow before it serves the file.
  • Record the URL, GUID, suggested filename, event state, and byte counts so a failed job can be diagnosed without treating it as a completed file.

The completion event arrives, but no file is found

  • Do not rely solely on filePath; the protocol does not guarantee that it is present or points to an existing file.
  • Check the configured absolute directory and inspect its contents for a GUID-named file or Chrome temporary partial files.
  • Wait for the file to appear and stabilize before renaming. A completed event should be followed by filesystem verification, not an assumption.

Chrome fails to launch on Ubuntu

  • Use Puppeteer’s supported browser/dependency installation instructions for Debian or Ubuntu.
  • Install required system dependencies with root privileges in the environment that runs Chrome.
  • Pin the browser and Puppeteer versions together, then validate the experimental download behavior against that installed browser.

The final name is wrong or overwrites another job’s output

  • Remember that suggestedFilename is only a suggestion; log it separately from the actual source path and final destination.
  • Sanitize names, preserve extensions where needed, and define whether duplicates get numbered suffixes or job identifiers.
  • For concurrent jobs, isolate output directories or coordinate destination selection rather than relying on a non-atomic existence check.

Reliability, performance, and cost considerations

Local Puppeteer automation has no per-screenshot API charge, but it does require a working browser installation, operating-system dependencies, disk space, and cleanup of downloaded files. For reliability, use a per-job directory, maintain event logs keyed by GUID, set explicit timeouts, and remove stale partial files only after determining they are not active downloads. If your jobs run concurrently, use separate directories or unique names to prevent collisions.

The CDP approach is appropriate when you need the actual file downloaded by a site and control of the filename after completion. Its key operational risk is browser/protocol compatibility: the download behavior is experimental in the protocol documentation and Puppeteer does not provide a documented high-level download handler. Pin versions and test the exact flow your automation depends on.

Frequently Asked Questions

Does Puppeteer have a documented `page.waitForDownload()` method?

No. Puppeteer’s Files guide says it currently does not offer a programmatic file-download handler; use CDP events and filesystem checks instead.

Can `allowAndName` set a custom filename such as `report.pdf`?

No. It uses a GUID-based name. Your script must map the completed download to a descriptive name and rename it afterward.

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.