🎯 A customizable, anti-detection cloud browser powered by self-developed Chromium designed for web crawlers and AI Agents.👉Try Now
Back to Blog

Puppeteer Download File: A Complete Guide for Node.js Developers

Alex Johnson
Alex Johnson

Senior Web Scraping Engineer

16-Jun-2026

TL;DR:

  • Downloads in headless Chrome are a CDP problem, not a Puppeteer-API problem. Puppeteer has no first-class page.download(); you drive the Chrome DevTools Protocol directly with Browser.setDownloadBehavior and listen for Browser.downloadProgress.
  • allowAndName plus eventsEnabled is the combination that actually reports completion. Setting the behavior alone silences the "save file" dialog; turning on events is what lets you await the finished byte count instead of guessing with a fixed sleep.
  • On a cloud browser the file lands server-side — so you fetch the bytes back through the page. A same-origin fetch() inside page.evaluate() returns the file as base64, which you decode and write to local disk. No headful machine, no shared volume.
  • Anti-bot and geo-gating apply to downloads too. Many gated files only serve to a residential IP in the right country; pinning egress with proxyCountry is part of making the download succeed at all.
  • The whole flow runs on Scrapeless Scraping Browser with one Puppeteer.connect() call. The cloud browser mints the session, applies anti-detection fingerprinting, and hands Puppeteer a standard WebSocket endpoint — your download code is plain CDP from there.
  • Free to start. New Scrapeless accounts include free Scraping Browser runtime — sign up at app.scrapeless.com.

Introduction: why "download a file with Puppeteer" is harder than it looks

Puppeteer gives Node.js developers full control over a Chrome page — clicking, typing, reading the DOM. File downloads are the one common task it has no dedicated method for. There is no await page.download(selector). Clicking a download link in a default headless session does one of two unhelpful things: the navigation is swallowed, or Chrome blocks the transfer because no download directory is configured.

The real control lives one layer down, in the Chrome DevTools Protocol (CDP). Browser.setDownloadBehavior tells Chrome where files go and whether to allow them at all; Browser.downloadWillBegin and Browser.downloadProgress report the lifecycle so you can wait for the exact moment a file finishes instead of sleeping and hoping.

Run this on your own machine and you still own two problems unrelated to CDP: the target site flags headless Chrome as a bot, and gated files often refuse anything that isn't a residential IP in the expected region. This guide runs the entire flow on top of Scrapeless Scraping Browser — an anti-detection cloud browser that mints the session, applies residential egress, and hands Puppeteer a normal WebSocket endpoint. The download code stays pure CDP; the detection and geo problems are handled by the runtime. There is one twist a cloud browser adds — the file downloads server-side — and the second half of this guide shows the pattern for pulling those bytes back to your local disk.


What You Can Do With It

  • Pull reports, exports, and statements behind a login — invoice PDFs, CSV exports, data-room files — once the session is authenticated.
  • Capture generated files that a page builds client-side from a blob (chart exports, "download CSV" buttons) and never exist as a static URL.
  • Grab gated documents that only serve to a residential IP in a specific country, by pinning egress before the request.
  • Wait deterministically for a transfer to finish using downloadProgress instead of a fragile fixed delay.
  • Scale the same script across many files or accounts without standing up headful machines — the browser is remote.

Why Scrapeless Scraping Browser

Scrapeless Scraping Browser is a customizable, anti-detection cloud browser designed for web crawlers and AI agents. For Puppeteer downloads specifically, it brings:

  • A standard Puppeteer connectionPuppeteer.connect() returns an ordinary Browser object, so every CDP call you already know works unchanged.
  • Residential proxies in 195+ countries — pin proxyCountry so gated files serve to an IP they trust.
  • Cloud-side anti-detection fingerprinting — the session looks like a real browser, so the download link is actually offered instead of being hidden behind a bot wall.
  • Session persistence — keep cookies and auth state warm across navigations, which is what makes logged-in downloads possible.
  • Self-developed Chromium — full CDP surface, so Browser.setDownloadBehavior and the download events behave exactly as documented.

Get your API key on the free plan at app.scrapeless.com.


Prerequisites

  • Node.js 18 or newer
  • A Scrapeless account and API key — sign up at app.scrapeless.com
  • Basic familiarity with Puppeteer and async/await

Install

1. Add the SDK and Puppeteer

The Scrapeless SDK mints the cloud session and connects Puppeteer to it. You need puppeteer-core (the protocol client without a bundled Chromium — the browser is remote):

bash Copy
npm install @scrapeless-ai/sdk puppeteer-core

2. Set your API key

Read it from the environment; never hardcode it:

bash Copy
export SCRAPELESS_API_KEY="your_api_token_here"

Configure: connect Puppeteer to the cloud browser

Puppeteer.connect() creates a Scrapeless session and returns a standard Puppeteer Browser. Pin the proxy country here so every request — including the download — egresses from the region you want:

javascript Copy
import { Puppeteer } from '@scrapeless-ai/sdk';

const browser = await Puppeteer.connect({
  apiKey: process.env.SCRAPELESS_API_KEY,
  sessionName: 'puppeteer-downloads',
  proxyCountry: 'US',
  sessionTTL: 300, // seconds the session stays alive
});

const page = await browser.newPage();

From here, browser and page are plain Puppeteer objects. Everything below is standard CDP.


Basic implementation: set download behavior and wait for completion

There are two CDP calls that matter and one event you wait on:

  1. Browser.setDownloadBehavior with behavior: 'allowAndName' allows the download and names the file by its GUID server-side. eventsEnabled: true is the part most guides miss — without it, no progress events fire and you're back to sleeping.
  2. Browser.downloadWillBegin tells you a transfer started, with the suggestedFilename and a guid.
  3. Browser.downloadProgress reports inProgresscompleted (or canceled), with receivedBytes and totalBytes.
javascript Copy
const cdp = await page.createCDPSession();

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

// Resolve only when the transfer actually finishes
const downloadComplete = new Promise((resolve) => {
  let meta = {};
  cdp.on('Browser.downloadWillBegin', (e) => {
    meta = { filename: e.suggestedFilename, guid: e.guid };
  });
  cdp.on('Browser.downloadProgress', (e) => {
    if (e.state === 'completed') {
      resolve({ ...meta, totalBytes: e.totalBytes });
    }
  });
});

Now trigger the download — clicking a button, a link, or, as here, a file the page generates client-side — and await the promise:

javascript Copy
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

// A "download CSV" button that builds the file in the browser from a blob
await page.evaluate(() => {
  const blob = new Blob(['col1,col2\n1,2\n3,4\n'], { type: 'text/csv' });
  const a = document.createElement('a');
  a.href = URL.createObjectURL(blob);
  a.download = 'report.csv';
  document.body.appendChild(a);
  a.click();
});

const result = await downloadComplete;
console.log(result);
// { filename: 'report.csv', guid: '845c9455-…', totalBytes: 18 }

The promise resolves the instant state === 'completed' arrives — no fixed sleep, and you get the real byte count back.

Get your API key on the free plan: app.scrapeless.com


The cloud twist: getting the bytes onto your local disk

On a local headful run, allowAndName writes the file to your downloadPath and you read it from disk. On a cloud browser the file lands on the remote session, not your machine — so downloadPath is server-side and your local /tmp/downloads stays empty.

The reliable pattern is to fetch the file's bytes inside the page context and return them as base64, then decode and write locally. The one rule: fetch() is subject to the same-origin policy, so navigate to the file's own origin first, then fetch it:

javascript Copy
import { writeFileSync } from 'node:fs';

const fileUrl = 'https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf';

// Land on the file's origin so fetch() is same-origin (no CORS block)
await page.goto(new URL(fileUrl).origin, { waitUntil: 'domcontentloaded' });

const out = await page.evaluate(async (url) => {
  const res = await fetch(url);
  const bytes = new Uint8Array(await res.arrayBuffer());
  let binary = '';
  for (let i = 0; i < bytes.length; i++) binary += String.fromCharCode(bytes[i]);
  return {
    status: res.status,
    type: res.headers.get('content-type'),
    b64: btoa(binary),
  };
}, fileUrl);

const buffer = Buffer.from(out.b64, 'base64');
writeFileSync('./dummy.pdf', buffer);
console.log({ status: out.status, type: out.type, bytes: buffer.length });
// { status: 200, type: 'application/pdf; qs=0.001', bytes: 13264 }

Because the fetch runs inside the authenticated page, it carries the session's cookies — so files behind a login download exactly like files in front of one. The CDP-event approach and this fetch approach are complementary: use the events to know when a click-triggered transfer finished; use the in-page fetch when you already have the file URL and want the bytes back locally.


Advanced patterns

Pin egress for gated files. If a document only serves to one region, set proxyCountry at connect time. The download then inherits that residential IP — no per-request proxy juggling.

Authenticate, then download. Keep the same page (and therefore the same session and cookies) for the login flow and the download. Scrapeless session persistence keeps the auth state warm across navigations, so the in-page fetch above sees a logged-in context.

Read progress for large files. Browser.downloadProgress also fires with state: 'inProgress' and a growing receivedBytes. Log the ratio against totalBytes to surface a real progress bar instead of a spinner.

Close the session when done. await browser.close() ends the cloud session promptly so you're not holding runtime you aren't using.


Troubleshooting

Symptom Cause Fix
No downloadProgress events ever fire eventsEnabled not set Pass eventsEnabled: true to Browser.setDownloadBehavior
Click navigates instead of downloading Server sends the file inline, not as an attachment Use the in-page fetch() retrieval pattern on the file URL
Local downloadPath is empty The file is on the remote cloud session, not your disk Fetch the bytes back through the page (see "the cloud twist")
fetch() throws a CORS error The page origin differs from the file origin page.goto(new URL(fileUrl).origin) before fetching
The download link never appears The site hid it behind a bot/geo wall Pin proxyCountry and let the cloud browser's fingerprinting render the real page

Conclusion: downloads as a first-class step in your pipeline

Downloading a file with Puppeteer comes down to three moves: tell Chrome to allow and report downloads (Browser.setDownloadBehavior with eventsEnabled), wait for the real completion event (Browser.downloadProgress), and — on a cloud browser — pull the bytes back through a same-origin in-page fetch. Running it on Scrapeless Scraping Browser folds the two hard parts that have nothing to do with CDP — bot detection and geo-gating — into the runtime, so the same script that works on a public file also works on a gated, logged-in one. For form-driven flows that lead up to a download, pair this with the Scrapling cloud-browser guide, and see the Scraping Browser product page and docs for the full CDP surface. Pin egress, keep the session warm for authenticated files, and wait on completed instead of a sleep.


Ready to Build Your AI-Powered Data Pipeline?

Join our community to claim a free plan and connect with developers building download and extraction pipelines: Discord · Telegram.

Sign up at app.scrapeless.com for free Scraping Browser runtime and adapt the patterns above to the files, regions, and logins your workflow needs. See pricing for scale.


FAQ

Q: Does Puppeteer have a built-in download method?
No. There is no page.download(). You configure downloads through the Chrome DevTools Protocol with Browser.setDownloadBehavior and observe them with the Browser.downloadWillBegin and Browser.downloadProgress events.

Q: Do I need a proxy to download files?
For public files, not strictly. For files gated by region or served only to residential IPs, yes — pin proxyCountry at connect time so the transfer egresses from an IP the site trusts.

Q: Why is my local download directory empty when running on a cloud browser?
Because the file downloads to the remote session, not your machine. Retrieve the bytes by fetching the file URL inside page.evaluate() and decoding the base64 locally, as shown above.

Q: How do I download a file that sits behind a login?
Authenticate first on the same page, then run the in-page fetch() — it carries the session cookies, so the file downloads as the logged-in user. Scrapeless session persistence keeps that auth state across navigations.

Q: How do I wait for the download to finish instead of guessing a timeout?
Listen for Browser.downloadProgress and resolve when state === 'completed'. The event includes receivedBytes and totalBytes, so you can also render real progress.

Q: Can I run this without an AI agent or extra tooling?
Yes. This is plain Puppeteer plus CDP on top of the Scrapeless session — no agent required. The SDK only mints the connection.

At Scrapeless, we only access publicly available data while strictly complying with applicable laws, regulations, and website privacy policies. The content in this blog is for demonstration purposes only and does not involve any illegal or infringing activities. We make no guarantees and disclaim all liability for the use of information from this blog or third-party links. Before engaging in any scraping activities, consult your legal advisor and review the target website's terms of service or obtain the necessary permissions.

Most Popular Articles

Catalogue