How to Fix Puppeteer User-Agent Settings With a Tor Proxy
Set Puppeteer’s User-Agent and Tor route independently, verify both, and fix authentication, proxy, and configuration errors.
Short answer: Puppeteer’s User-Agent and proxy route are separate settings. Set the User-Agent with the current page.setUserAgent(options) API before navigation, and route Chromium traffic at launch with --proxy-server=.... Changing one does not change the other.
The current Page API documents setUserAgent(options); the older setUserAgent(userAgent, userAgentMetadata) signature is deprecated. The proxy must use a scheme Chromium supports, such as HTTP, HTTPS, SOCKS4 or SOCKS5. Chrome’s SOCKS5 implementation does not support SOCKS authentication through page.authenticate().
1. Minimal working pattern
Use a proxy address and port supplied by your Tor setup. Do not assume a universal Tor port: the correct value depends on how Tor is installed and configured on your machine.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: ['--proxy-server=socks5://127.0.0.1:TOR_SOCKS_PORT']
});
try {
const page = await browser.newPage();
await page.setUserAgent({
userAgent: 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/TEST Safari/537.36'
});
await page.goto('https://example.com', {
waitUntil: 'domcontentloaded',
timeout: 60000
});
console.log('Configured User-Agent:', await page.evaluate(() => navigator.userAgent));
console.log('Title:', await page.title());
} finally {
await browser.close();
}
})();
Replace TOR_SOCKS_PORT with the port exposed by your local Tor service. The Puppeteer Page API covers the User-Agent method, while the proxy guide covers launch arguments and proxy behavior.
2. Configure each setting in the right place
| Goal | Setting | When it takes effect |
|---|---|---|
| Change the browser-visible User-Agent | page.setUserAgent({ userAgent }) |
Set it before goto() or any request that must carry it |
| Route Chromium traffic through Tor | args: ['--proxy-server=SCHEME://HOST:PORT'] |
At puppeteer.launch() |
| Configure Puppeteer’s browser download | HTTP_PROXY, HTTPS_PROXY, NO_PROXY |
Environment configuration for downloads, not a replacement for a browser proxy |
A User-Agent string only changes request-identification data. It does not change your outbound IP address or network path. A proxy argument changes the route, but does not select a User-Agent.
3. Choose the proxy scheme and authentication model
HTTP or HTTPS proxy
const browser = await puppeteer.launch({
args: ['--proxy-server=http://proxy.example:PORT']
});
If the HTTP proxy requires credentials, authenticate with Puppeteer after creating the page:
await page.authenticate({
username: process.env.PROXY_USERNAME,
password: process.env.PROXY_PASSWORD
});
Proxy authentication is different from HTTP authentication required by the website. One page.authenticate() credential pair cannot represent two different pairs at the same time.
SOCKS4 or SOCKS5
const browser = await puppeteer.launch({
args: ['--proxy-server=socks5://127.0.0.1:TOR_SOCKS_PORT']
});
Chrome supports SOCKS4 and SOCKS5 proxy schemes, but Chrome’s SOCKS5 implementation does not support SOCKS authentication through page.authenticate(). If your SOCKS endpoint requires credentials, do not expect that API to supply them. Use a compatible network arrangement or an authenticated proxy mechanism that Chromium supports.
4. Verify the User-Agent and route independently
Checking navigator.userAgent only proves what JavaScript sees. It does not prove that the destination received the expected header or that traffic used Tor. Verify both outcomes separately.
Check the value inside the page
const observedInBrowser = await page.evaluate(() => navigator.userAgent);
console.log(observedInBrowser);
Check what a destination receives
Navigate to an HTTP request-inspection or echo endpoint that you control or trust, then inspect its request headers. Confirm the received User-Agent value after navigation.
Check the outbound route
Navigate to a trusted IP-address endpoint and compare the reported address with the route you expect. The Puppeteer proxy guide also recommends testing the proxy independently with cURL, which helps separate a Tor problem from a Puppeteer problem.
curl --proxy 'socks5://127.0.0.1:TOR_SOCKS_PORT' https://your-trusted-ip-endpoint.example
Use the same proxy scheme, host and port in both tests. A successful page load alone does not prove that every request used the intended route.
5. Complete diagnostic script
This script records the configured User-Agent, logs failed requests, and checks two independent URLs. Replace the placeholders with endpoints appropriate for your environment.
const puppeteer = require('puppeteer');
const proxy = 'socks5://127.0.0.1:TOR_SOCKS_PORT';
const userAgent = 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Chrome/TEST Safari/537.36';
(async () => {
const browser = await puppeteer.launch({
headless: true,
args: [`--proxy-server=${proxy}`]
});
try {
const page = await browser.newPage();
page.on('requestfailed', request => {
console.error('Request failed:', request.url(), request.failure());
});
page.on('response', response => {
if (response.status() >= 400) {
console.error('HTTP error:', response.status(), response.url());
}
});
await page.setUserAgent({ userAgent });
await page.goto('https://example.com', { waitUntil: 'networkidle2', timeout: 60000 });
console.log({
navigatorUserAgent: await page.evaluate(() => navigator.userAgent),
title: await page.title(),
url: page.url()
});
await page.goto('https://your-trusted-ip-endpoint.example', {
waitUntil: 'domcontentloaded',
timeout: 60000
});
console.log('IP-check response:', await page.evaluate(() => document.body.innerText));
} finally {
await browser.close();
}
})();
6. Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| User-Agent is unchanged | setUserAgent() ran after navigation, or the old API signature is being used incorrectly |
Call await page.setUserAgent({ userAgent: '...' }) immediately after creating the page and before navigation. Check the docs for your installed Puppeteer release. |
| Requests use the wrong IP | The launch argument is missing, malformed, or points to the wrong endpoint | Use --proxy-server=SCHEME://HOST:PORT, then verify with an IP endpoint and an independent cURL request. |
ERR_PROXY_CONNECTION_FAILED |
Tor or the proxy is not listening, or the host and port are wrong | Confirm the local service is running, confirm its configured listener, and test the endpoint outside Puppeteer. |
| SOCKS5 credentials do not work | Chrome does not support SOCKS authentication through page.authenticate() |
Do not pass SOCKS credentials with that API. Use a supported authenticated proxy arrangement. |
| Website credentials are rejected | Proxy credentials and website HTTP-auth credentials are being conflated | Identify which server is requesting authentication. Puppeteer cannot express two different credential pairs through one page.authenticate() call. |
| Environment proxy variables have no effect | HTTP_PROXY, HTTPS_PROXY and NO_PROXY are being used as browser-routing settings |
Those settings are discussed for browser downloads. Configure browser traffic with --proxy-server. |
| Configuration file is ignored | The project uses puppeteer-core |
The official configuration guide says configuration files and environment variables are ignored by puppeteer-core. Pass launch options explicitly. |
| Pages intermittently time out | Tor adds latency, or the target blocks the route | Use a suitable navigation timeout, log failed requests, wait for the minimum required readiness event, and test the same URL through the proxy with cURL. |
7. Browser-wide versus per-page routing
The --proxy-server launch argument applies to the Chromium browser process, so it is the straightforward choice when all pages should use the same route. If you need different routes at the same time, investigate a context or request-routing design supported by your Puppeteer version and verify it carefully.
Per-page proxy plugins can intercept requests and add overhead. Compatibility is version-sensitive, so check the plugin’s current documentation before making it part of a production path.
8. Timing, reliability and cost considerations
- Set before navigation: configure the User-Agent before the first request whose headers matter.
- Use explicit timeouts: Tor can make DNS, connection and page-load phases slower. Choose a timeout that matches the page and log failures.
- Wait for the right event:
domcontentloadedis faster than waiting for every resource;networkidle2can be useful for pages that load content asynchronously but may take longer. - Separate diagnosis: test the proxy with cURL, test the received User-Agent at the destination, and then test Puppeteer. This identifies which layer is failing.
- Reuse a browser when appropriate: launching Chromium is more expensive than opening another page. Reuse a browser for compatible jobs, but isolate sessions when cookies or identities must not mix.
- Do not infer anonymity from a string: a custom User-Agent does not make a route private, and a Tor route does not make the browser fingerprint identical to another browser.
9. Or skip the browser setup
If your goal is a clean screenshot rather than browser-network debugging, ScreenshotNeo provides a single API request. Its API accepts a URL and returns PNG, JPEG, WebP or PDF; the API documentation lists the options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups and chat widgets are removed before the shot. Bot checks, blank pages and failed loads are never billed, and response headers identify the page verdict and whether it was billed. ScreenshotNeo also has an MCP server so Claude, Cursor and other MCP clients can take screenshots. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with the 1,000-shot monthly allowance.
10. FAQ
Can I set the User-Agent in the Chromium launch arguments?
Use page.setUserAgent(options) for a page-level setting. Keep it separate from the launch-time proxy argument.
Does a Tor proxy automatically change navigator.userAgent?
No. The proxy changes routing; the User-Agent must be configured separately.
Should I use HTTP_PROXY instead of --proxy-server?
No for browser traffic. Puppeteer documents those environment variables for download configuration. Use --proxy-server to route Chromium requests.
Why does an IP check pass while the target site still blocks me?
An IP check confirms one route. A target can apply separate policy to the destination, request headers, browser behavior or the Tor exit route. Verify the received User-Agent and inspect failed responses at the target.
Is the old two-argument User-Agent method safe to keep?
The current Page API marks that signature deprecated. Update to setUserAgent({ userAgent, ... }) and follow the documentation for the Puppeteer version installed.


