Integrate proxies with Node.js using Axios, Puppeteer, and Playwright. Covers SOCKS support, rotation wrappers, connection pooling, and error handling.
Proxy Configuration in Axios: httpsAgent and Proxy-Agent
The reliable approach for HTTPS proxy support is to use a custom httpsAgent. Install the https-proxy-agent package from npm, create an HttpsProxyAgent instance with your proxy URL (including credentials), and pass it as the httpsAgent property in your Axios request config or when creating an Axios instance. This agent handles the CONNECT tunneling required for proxying HTTPS traffic correctly.
For a more versatile solution, the proxy-agent package provides a universal agent that automatically selects the correct proxy protocol based on the proxy URL scheme — HTTP, HTTPS, SOCKS4, or SOCKS5. This is particularly useful when your application needs to support multiple proxy types configurable at runtime, because you do not need to change the agent class when switching between proxy protocols.
When creating an Axios instance with a proxy agent, the agent is reused across all requests made through that instance. This enables connection pooling to the proxy server, reducing TCP handshake overhead for subsequent requests. For high-throughput applications, create a dedicated Axios instance per proxy endpoint rather than recreating agents per request — the connection pooling improvement alone can cut request latency by 30-50ms per request.
Using Proxies with node-fetch and Undici
Node.js 18+ introduced a built-in fetch based on Undici, and its proxy configuration takes a completely different path. Undici uses a dispatcher-based architecture where you create a ProxyAgent (from the undici package) with your proxy URL and pass it as the dispatcher option. This dispatcher handles all connection management including the proxy tunnel setup. The Undici ProxyAgent supports connection pooling, HTTP/2, and automatic keep-alive — features that make it competitive with dedicated HTTP client libraries for proxy workloads.
One critical difference between node-fetch agents and Undici dispatchers is how they handle proxy authentication. node-fetch agents accept credentials embedded in the proxy URL string, the familiar protocol://user:pass@host:port format. Undici's ProxyAgent also accepts URL-embedded credentials, but additionally supports a token property for custom authentication headers and an interceptors array for modifying requests before they reach the proxy — useful for providers that require custom header-based authentication.
For new projects, the built-in fetch with Undici's ProxyAgent is the recommended path. It eliminates the external dependency on node-fetch, integrates with Node.js's native networking stack, and receives performance optimizations with each Node.js release. For existing projects using node-fetch v2, the https-proxy-agent approach remains stable and well-supported.
Puppeteer Proxy Setup and Authentication
The authentication challenge with Puppeteer proxies mirrors Chrome's limitation: the --proxy-server flag does not accept credentials. When the browser encounters a proxy authentication challenge, it displays a dialog that Puppeteer cannot interact with directly through the launch arguments. The solution is page.authenticate, a Puppeteer method that pre-registers credentials for proxy authentication. Call page.authenticate with your username and password object before navigating to any URL, and Puppeteer handles the 407 challenge automatically.
A critical detail: page.authenticate must be called before the first navigation and applies to the specific page object. If you open new pages or tabs, you must call authenticate on each one individually. For applications that create many pages, build a helper function that wraps page creation with automatic authentication setup to prevent missed calls.
For per-page proxy switching — running different pages through different proxies — Puppeteer does not support this natively because the proxy is set at the browser level. The workaround is launching multiple browser instances, each with a different proxy. While this consumes more memory (each Chromium instance uses 50-100MB), it provides complete proxy isolation between sessions and prevents cross-contamination of cookies or session state.
Playwright's Superior Proxy Model: Per-Context Configuration
At the browser level, pass a proxy object in the launch options with server, username, and password properties. Unlike Puppeteer, Playwright accepts authentication credentials directly in the launch configuration — no need for page.authenticate workarounds. The proxy applies to all contexts created from this browser instance by default.
The real power comes from per-context proxies. When creating a new browser context with browser.newContext, you can specify a different proxy object that overrides the browser-level proxy for that context. This means a single browser instance can have multiple contexts, each using a different proxy with different credentials. Each context maintains its own cookies, storage, and proxy connection independently. You can run 10 contexts through 10 different country-specific proxies simultaneously, all within one browser process — dramatically more memory-efficient than Puppeteer's one-browser-per-proxy approach.
Playwright also supports the bypass property in the proxy configuration, which accepts a comma-separated list of domains that should connect directly without going through the proxy. This is useful for skipping the proxy on non-essential resource domains like CDN assets or analytics endpoints, reducing proxy bandwidth usage without affecting the primary scraping workflow.
For SOCKS proxy support, Playwright accepts socks5:// URLs in the server property. This works at both browser and context levels, and Playwright handles the SOCKS handshake and authentication internally without external packages.
SOCKS Proxy Support in Node.js Applications
The socks-proxy-agent package is the standard solution for adding SOCKS support to any Node.js HTTP client that accepts custom agents. Install the package, create a SocksProxyAgent with your SOCKS proxy URL (socks5://user:pass@host:port), and pass it as the agent or httpsAgent option. This agent works with Axios, node-fetch, the built-in http/https modules, and any library that supports Node.js agents. For SOCKS4 proxies, use the socks4:// scheme in the URL.
SOCKS5 proxies have an important advantage over SOCKS4: they support UDP traffic and can perform DNS resolution on the proxy server side. When you use a SOCKS5 proxy with remote DNS resolution, your DNS queries go through the proxy rather than your local DNS server. This prevents DNS leaks that could expose the domains you are accessing. The socks-proxy-agent enables remote DNS by default for SOCKS5 connections.
Performance considerations with SOCKS differ from HTTP proxies. SOCKS proxies add slightly more latency on initial connection because the SOCKS handshake requires an additional round-trip before the TCP connection is established. However, once the connection is open, data transfer speeds are identical. For high-throughput applications, maintain persistent connections to SOCKS proxies through connection pooling rather than opening a new SOCKS connection per request.
Proxy Integration with Got: Another Popular HTTP Client
Got accepts an agent option that maps protocol schemes to agent instances. For proxy support, create separate agents for HTTP and HTTPS targets: an HttpProxyAgent for http:// targets and an HttpsProxyAgent for https:// targets. Pass them in the agent object with http and https keys. This explicit per-protocol agent mapping avoids the confusion that Axios's proxy option creates, because you always know exactly which agent handles which protocol.
Got's hooks system provides an elegant way to implement proxy rotation without wrapping the client. The beforeRequest hook fires before every outgoing request, and you can use it to dynamically assign different proxy agents to different requests. Create a hook function that selects a proxy from your pool, instantiates the appropriate agent, and assigns it to the request options. This keeps rotation logic centralized and separate from your application code.
The retry configuration in Got is particularly valuable for proxy workflows. Got supports per-status-code retry limits, exponential backoff, and custom retry logic through the beforeRetry hook. Configure Got to retry on 403, 429, and 502 status codes — the common proxy-related failure codes — and use the beforeRetry hook to switch to a different proxy agent before each retry attempt. This creates an automatic rotation-on-failure pattern with minimal code. Set the retry limit to 3-4 and the maximum retry delay to 10 seconds to prevent aggressive retry storms.
Building a Proxy Rotation Wrapper in Node.js
The wrapper should maintain a pool of proxy entries, each containing the proxy URL, a health score, a last-used timestamp, and a per-domain cooldown map. When a caller requests a proxy (optionally specifying a target domain), the wrapper filters out proxies that are currently in cooldown for that domain, sorts by health score, and returns the best available option. After each request, the caller reports success or failure back to the wrapper, which updates the health score — increment on success, decrement on failure, and temporarily remove proxies whose health drops below a threshold.
Implement the pool as a class with getProxy(domain), reportSuccess(proxyUrl, domain), and reportFailure(proxyUrl, domain, statusCode) methods. The getProxy method should implement weighted random selection rather than strict round-robin — always picking the highest-scored proxy creates hotspots, while pure random ignores health data. Weighted random selection gives healthier proxies a higher probability of selection while still distributing load across the pool.
For backconnect proxy endpoints that handle rotation server-side, the wrapper simplifies to session management. Track active sessions by identifier, enforce session TTLs, and provide a method to get a fresh session ID when the current one expires. Even with backconnect proxies, the health tracking and cooldown logic remains valuable — if a backconnect session consistently fails on a specific domain, you should flag it and request a new session rather than retrying indefinitely.
Export the wrapper as a singleton module so all parts of your application share the same pool state and health data.
Error Handling and Retry Patterns for Node.js Proxy Applications
Network-level errors — ECONNREFUSED, ECONNRESET, ETIMEDOUT — indicate that the connection to the proxy server itself failed. ECONNREFUSED means the proxy server is not accepting connections on that port, often because the server is down or you have the wrong port. ECONNRESET means the proxy server dropped the connection mid-stream, usually due to overload. ETIMEDOUT means the proxy server did not respond within your configured timeout. For all three, retry with the same proxy once (transient network issues are common), then switch to a different proxy on the second attempt.
Proxy-level errors are HTTP responses from the proxy gateway rather than the target site. HTTP 407 indicates authentication failure — check credentials, do not retry. HTTP 502 from the proxy means the proxy could not reach the target — the target may be blocking the proxy IP or the target is down. HTTP 503 from the proxy means the proxy service itself is overloaded — back off and retry after a delay.
Target-level errors pass through the proxy transparently. HTTP 403 from the target means your proxy IP was detected and blocked — rotate immediately. HTTP 429 means rate limiting — implement exponential backoff starting at 2 seconds, doubling up to 30 seconds. HTTP 200 with CAPTCHA content (detect by checking response body for CAPTCHA markers) means the target is challenging the proxy IP — rotate to a residential or mobile proxy if you were using datacenter.
Implement a circuit breaker pattern: if a proxy fails 5 consecutive requests within 60 seconds, remove it from the active pool for 5 minutes. This prevents wasting requests on a proxy that is clearly compromised while allowing recovery after the cooldown period.
Performance Tuning: Connection Pooling and Keep-Alive with Proxies
Node.js HTTP agents manage connection pools automatically, but the defaults are conservative. The built-in http.Agent defaults to maxSockets of Infinity (no limit) but does not enable keep-alive by default in older Node.js versions. When using proxy agents like HttpsProxyAgent, configure keepAlive to true and set maxSockets to match your proxy provider's concurrency limit. A typical configuration for a residential proxy plan with 100 concurrent connections would set maxSockets to 100 and maxFreeSockets to 20 to maintain a warm pool without hoarding idle connections.
The keepAliveMsecs option controls how long idle connections stay in the pool before being closed. Set this to match your proxy provider's connection timeout — typically 30-60 seconds. If your keep-alive interval is longer than the proxy server's timeout, connections will be closed server-side but remain in your pool, causing ECONNRESET errors on the next request that tries to reuse them. Setting keepAliveMsecs to 80% of the server's timeout provides a safe margin.
For Puppeteer and Playwright, connection pooling is handled at the browser level and generally does not require manual tuning. However, avoid creating and destroying browser instances for each request — this is the browser automation equivalent of disabling connection pooling. Instead, reuse browser contexts and only create new ones when you need a fresh session or a different proxy.
Testing Proxy Connectivity in Your Node.js Application
Build a validateProxy function that takes a proxy URL and performs three checks. First, a connectivity test: make a simple GET request through the proxy to an IP echo service. If this fails with a connection error, the proxy is unreachable — check the host, port, and your network's firewall rules. Second, an authentication test: verify the response is not a 407. If it is, your credentials are wrong or the proxy requires a different authentication method. Third, a geo-verification test: parse the returned IP and check its geographic location against your expected proxy location. If you requested a US proxy but the IP geolocates to Germany, there is a configuration issue with your provider or you are connecting to the wrong endpoint.
Run health checks on a schedule during operation, not just at startup. Create a background interval (every 60-120 seconds) that validates a random sample of your proxy pool. This detects proxy degradation — IPs that become blacklisted, servers that slow down, or entire proxy endpoints that go offline — before it impacts your scraping results.
Log proxy performance metrics continuously: response time, success rate, and bandwidth used per proxy. Aggregate these metrics over sliding windows (last 5 minutes, last hour) and expose them through your application's metrics endpoint. These metrics drive your rotation wrapper's health scoring and provide the data needed to identify when a proxy provider's quality changes. Set up alerts for success rate drops below 90% or average response time spikes above 3 seconds.