Puppeteer Block Requests: Images, Fonts, Ads, Trackers
Block requests in Puppeteer with your own interception code: by resource type or host, with measured bytes and load time saved per type, and a CDP option.
To block requests in Puppeteer, turn on request interception and decide for each request whether to abort or continue it:
await page.setRequestInterception(true); page.on('request', (req) => { if (req.resourceType() === 'image') req.abort(); else req.continue(); });
That is the whole mechanism. The useful questions are what to block for a screenshot, what each block actually saves, and what it costs the picture. This post answers those with numbers from a test page, then gives you a complete blocker you can paste into a project. It belongs to our Puppeteer screenshot guide.
Tested with: Puppeteer 25.14.0, Chrome 155.0.8059.79, Node.js 22.18.0, macOS on an Apple M1 Pro. Tested on 2026-10-18.
What blocking saves, per resource type
We served a test page from a local Node.js server. It has a 28 KB stylesheet, two web fonts (about 750 KB each), twelve 53 KB PNG images, a 1.5 MB video with preload="auto", and two 90 KB third-party scripts loaded from a second origin. The server adds a fixed delay to each file type (images 120 ms, fonts 150 ms, scripts 400 ms) so the timing is repeatable.
For each rule we ran 5 fresh browser contexts and took the median. "Transferred" is the sum of encodedDataLength from the Chrome DevTools Protocol Network.loadingFinished events, measured at the load event. We ran the test twice: once on loopback (no bandwidth limit) and once throttled with Network.emulateNetworkConditions to 10 Mbit/s and 40 ms latency, which is closer to a real fetch over the internet.
| Blocked | Requests blocked | Transferred (loopback) | Load, loopback | Load, 10 Mbit/s | Screenshot PNG |
|---|---|---|---|---|---|
| Nothing | 0 | 3,765 KB | 457 ms | 2,966 ms | 51 KB |
image |
12 | 3,137 KB | 439 ms | 2,464 ms | 11 KB |
font |
2 | 2,298 KB | 436 ms | 1,833 ms | 51 KB |
media |
1 | 2,300 KB | 440 ms | 2,184 ms | 51 KB |
stylesheet |
1 | 3,738 KB | 454 ms | 2,987 ms | 51 KB |
Third-party script |
2 | 3,589 KB | 418 ms | 2,824 ms | 51 KB |
image + font + media |
15 | 205 KB | 441 ms | 695 ms | 11 KB |
What we took from it:
- On a fast network, blocking barely changes load time. Loopback load times stayed between 418 and 457 ms whatever we blocked, because the slowest file (a third-party script) sets the pace. Blocking pays off when bandwidth is the limit.
- On a throttled network the big files dominate. Blocking fonts, images and video together cut load from about 3.0 s to 0.7 s. Transfer at
loadfell from 3.7 MB to 205 KB on loopback, and from 2.3 MB to 205 KB on the throttled link, where less of the video had arrived by then. - The video was the largest file but it did not hold
load. Browsers do not wait for media data beforeload. Blocking it still saves bandwidth, and the saving shows in the throttled column because the video competes with everything else for the same link. - Blocking images changes the picture. The PNG dropped from 51 KB to 11 KB because the image boxes were empty. Fonts and stylesheets also change the picture: blocked fonts fall back to a system font, and a blocked stylesheet gives you unstyled HTML. Only ads, trackers, chat widgets and media are usually safe to drop for a screenshot.
The cost of interception itself
Request interception sends every request from Chrome to your Node.js process and waits for an answer. We measured that cost on the same page with 9 runs per mode:
| Mode | Median load (loopback) |
|---|---|
| No interception | 420 ms |
| Interception on, every request continued | 429 ms |
| Interception on, images, fonts and media aborted | 426 ms |
Network.setBlockedURLs (CDP), same files blocked |
416 ms |
On 20 requests the overhead is about 10 ms. It grows with the request count, and it grows faster when your Node.js event loop is busy with other pages, which is exactly the situation in a parallel screenshot service.
A complete blocker
This script blocks by resource type and by host, counts what it did, and exits with a non-zero code when the capture fails. The host list matches a domain and all its subdomains.
import puppeteer from 'puppeteer'; const BLOCKED_TYPES = new Set(['media', 'font']); const BLOCKED_HOSTS = ['doubleclick.net', 'google-analytics.com', 'googletagmanager.com', 'hotjar.com', '127.0.0.1']; function shouldBlock(req) { if (BLOCKED_TYPES.has(req.resourceType())) return true; const { hostname } = new URL(req.url()); return BLOCKED_HOSTS.some((h) => hostname === h || hostname.endsWith(`.${h}`)); } const url = process.argv[2] ?? 'https://example.com'; const browser = await puppeteer.launch(); try { const page = await browser.newPage(); const stats = { blocked: 0, allowed: 0 }; await page.setRequestInterception(true); page.on('request', (req) => { if (req.isInterceptResolutionHandled()) return; if (shouldBlock(req)) { stats.blocked++; req.abort('blockedbyclient', 0); } else { stats.allowed++; req.continue({}, 0); } }); await page.goto(url, { waitUntil: 'load', timeout: 30_000 }); await page.screenshot({ path: 'blocked.png' }); console.log(stats); } catch (err) { console.error(err); process.exitCode = 1; } finally { await browser.close(); }
On our test page it printed { blocked: 5, allowed: 15 } (two fonts, one video, two scripts from the second origin, which is 127.0.0.1 in the list only for the test). On https://example.com it printed { blocked: 0, allowed: 2 }. Remove 127.0.0.1 from the list before you use it.
Three details in that handler matter:
isInterceptResolutionHandled()returns early if another handler already resolved the request. Without it, two handlers that both callcontinue()throw "Request is already handled".- The second argument to
abort()andcontinue()is a priority. It turns on Puppeteer's cooperative intercept mode, where several handlers can vote and the highest priority wins. If you are the only handler, the plain calls work too. 'blockedbyclient'is the error code the page sees. It matches what an ad blocker produces, so page code that checks for blocked trackers behaves as it would for a real visitor.
Block by URL pattern instead of host
When a vendor serves trackers from its main domain, match on the path:
const BLOCKED_PATHS = [/\/collect\b/, /\/pixel\b/, /\/beacon\b/]; const isTrackingPath = (req) => BLOCKED_PATHS.some((re) => re.test(new URL(req.url()).pathname));
Answer instead of abort
Some pages break when a script fails to load: an onerror handler shows a warning, or a script waits for a global the blocked file would define. Answer with an empty response instead, so the page sees success:
page.on('request', (req) => { if (req.isInterceptResolutionHandled()) return; if (shouldBlock(req) && req.resourceType() === 'script') { req.respond({ status: 200, contentType: 'application/javascript', body: '' }, 0); } else if (shouldBlock(req)) { req.abort('blockedbyclient', 0); } else { req.continue({}, 0); } });
Without interception: Network.setBlockedURLs
Chrome can block URLs itself through the DevTools Protocol. No request makes a round trip to Node.js, and you do not have to resolve every request. The method is marked experimental in the protocol, so pin your Puppeteer version and re-test on upgrades.
import puppeteer from 'puppeteer'; const url = process.argv[2] ?? 'https://example.com'; const browser = await puppeteer.launch(); try { const page = await browser.newPage(); const cdp = await page.createCDPSession(); await cdp.send('Network.enable'); // Chrome matches these patterns itself; no request round-trips to Node. await cdp.send('Network.setBlockedURLs', { urls: ['*doubleclick.net*', '*google-analytics.com*', '*.mp4', '*.woff2', '*.ttf'], }); let failed = 0; page.on('requestfailed', () => failed++); await page.goto(url, { waitUntil: 'load', timeout: 30_000 }); await page.screenshot({ path: 'blocked-cdp.png' }); console.log({ blocked: failed }); } catch (err) { console.error(err); process.exitCode = 1; } finally { await browser.close(); }
The trade-off: patterns are simple wildcards on the URL, so you cannot block by resource type, and you cannot respond with an empty body. Use it for a fixed list of hosts and file extensions; use interception when the decision needs logic.
What to block for screenshots
| Category | Block? | Why |
|---|---|---|
| Ad networks | Yes | Ads change on every load, push content down, and are a common source of slow third-party requests |
| Analytics and tracking | Yes | Invisible in the picture; they also keep connections open, which can delay network idle waits |
| Chat widgets | Yes | They cover content and often open long-lived connections |
| Video and audio | Usually | Large, and a paused video still shows its poster when the poster is an image |
| Images | Only for text captures | Blocking them empties the image boxes in the screenshot |
| Web fonts | Rarely | The text renders in a fallback font and the layout can shift |
| Stylesheets | No | The page renders unstyled |
| First-party scripts | No | Client-rendered pages render nothing without them |
Cookie banners are a different problem: blocking the consent script often leaves an empty overlay or a page that refuses to scroll. Our post on blocking cookie banners at scale covers that case. For Puppeteer code that removes them, see hiding cookie banners and chat widgets in Puppeteer.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Page never finishes loading | A request reached the handler and was never resolved | Make every branch call abort(), continue() or respond() |
Error: Request is already handled! |
Two handlers resolved the same request | Check req.isInterceptResolutionHandled() first, and pass a priority |
| A rule never matches | You compared req.url() as a string and the URL has a port, query or uppercase host |
Parse with new URL(req.url()) and compare hostname |
| Two handlers fight over requests | A library you use also intercepts | Use cooperative mode: pass a priority to every abort(), continue() and respond() call |
| Page shows "please disable your ad blocker" | The page detects failed tracker scripts | Respond with an empty script instead of aborting |
| Layout broken after blocking | You blocked a stylesheet, font or first-party script | Narrow the rule to third-party hosts |
| Fewer bytes saved than expected | Service worker served the files from its cache | Call page.setBypassServiceWorker(true) before goto() |
| Blocking had no effect on load time | Network is fast and one slow file sets the pace | Expected; the savings show on slow or metered networks |
The same blocking with one API call
RenderScreenshot has the common categories built in, plus your own patterns and resource types (see blocking options). Ads, trackers, chat widgets and cookie banners use lists we maintain, and blockUrls() takes glob patterns:
import { writeFile } from 'node:fs/promises'; import { Client, TakeOptions } from 'renderscreenshot'; const client = new Client(process.env.RS_API_KEY); const image = await client.take( TakeOptions.url('https://example.com') .blockAds() .blockTrackers() .blockChatWidgets() .blockUrls(['*hotjar*', '*/beacon/*']) .blockResources(['media']) ); await writeFile('clean.png', image);
The ads and cookie banner guide shows which option fits which kind of site. Create a free account to get 150 credits every month and try it on your own URLs.
Related guides
- Taking screenshots with Puppeteer, the hub for this series
- When is a page ready? Puppeteer waitUntil compared
- Scaling Puppeteer screenshots, where per-request overhead adds up
- Blocking cookie banners at scale
Related in this topic
- Puppeteer Screenshots: Tested Recipes for Puppeteer 25 Every Puppeteer screenshot recipe re-run on Puppeteer 25.14 and Chrome 155: viewport, full page, element, clip, waits, formats, auth, blocking and fonts.
- Puppeteer on AWS Lambda with @sparticuz/chromium Run Puppeteer on AWS Lambda with @sparticuz/chromium: version matching, a container-image handler tested in the Lambda emulator, and warm reuse.
- Puppeteer in Docker: A Tested Dockerfile with Fonts A non-root Puppeteer Dockerfile with Chromium, CJK and emoji fonts and dumb-init, plus the sandbox error it hits, measured image sizes and launch times.
- Puppeteer Full Page Screenshot: Limits, Lazy Images, Fixes Puppeteer full page screenshot guide: the 16,384 px limit we measured, captureBeyondViewport, lazy-load scrolling, sticky headers and a tested script.
- Hide Cookie Banners and Chat Widgets in Puppeteer Remove cookie banners, consent iframes, shadow-DOM dialogs and chat widgets from Puppeteer screenshots: a tested module and what each method misses.
- Puppeteer HTML to PDF: page.pdf() Options Tested Generate PDFs with Puppeteer page.pdf(): paper size, margins, header and footer templates, page breaks and print CSS, with measured sizes and timings.