<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Miller James</title>
    <description>The latest articles on DEV Community by Miller James (@miller_proxy).</description>
    <link>https://dev.to/miller_proxy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3629795%2F0a06d020-5c62-45c4-aa24-fed9202af042.jpeg</url>
      <title>DEV Community: Miller James</title>
      <link>https://dev.to/miller_proxy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/miller_proxy"/>
    <language>en</language>
    <item>
      <title>Load Balancing Across a Residential Proxy Pool in Production</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Mon, 20 Apr 2026 01:24:28 +0000</pubDate>
      <link>https://dev.to/miller_proxy/load-balancing-across-a-residential-proxy-pool-in-production-2326</link>
      <guid>https://dev.to/miller_proxy/load-balancing-across-a-residential-proxy-pool-in-production-2326</guid>
      <description>&lt;p&gt;A production residential proxy pool should optimize for stable throughput, controlled per-IP pressure, and fast recovery from bad nodes. It should not aim for perfectly equal request counts, because residential endpoints vary in latency, availability, and session stability in ways homogeneous server pools usually do not. &lt;/p&gt;

&lt;p&gt;That’s why plain round robin is a weak default. Weighted or score-based routing works better because it can shift traffic away from proxies that are overloaded, degraded, or temporarily unavailable. &lt;/p&gt;

&lt;p&gt;There’s also an important compliance line here. If a target starts returning 429 or repeated 403 responses, the correct move is to reduce or suspend traffic for that target and review your allowed request budget, not to use routing logic to keep pressing the same target through other IPs. [&lt;/p&gt;

&lt;h2&gt;
  
  
  What does “balanced” mean in a residential proxy pool?
&lt;/h2&gt;

&lt;p&gt;Balanced means traffic is distributed according to each proxy’s current health and usable capacity, not split into identical request counts. &lt;/p&gt;

&lt;p&gt;For a real residential proxy pool, measure these fields per proxy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Success rate over a rolling window.&lt;/li&gt;
&lt;li&gt;P95 latency.&lt;/li&gt;
&lt;li&gt;Current in-flight requests.&lt;/li&gt;
&lt;li&gt;Recent timeout and connection-error count.&lt;/li&gt;
&lt;li&gt;Recent 429 and 403 signals by target host, when those signals apply to your workflow. 
If you don’t record those metrics, you can still rotate proxies, but you can’t prove you’re balancing them safely. That’s the difference between a proxy list and a production scheduler. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-pool-management" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What components do you need in production?
&lt;/h2&gt;

&lt;p&gt;You need five components: a proxy registry, a scheduler, a feedback loop, a session-affinity map, and metrics storage. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-usage" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The proxy registry tracks endpoint details and live state such as &lt;code&gt;in_flight&lt;/code&gt;, recent outcomes, and cooldown status. The scheduler selects the next proxy. The feedback loop updates proxy health after each request. The session-affinity map preserves continuity for workflows that need it. Metrics storage gives you the evidence to verify that load distribution is improving rather than just moving failures around. &lt;a href="https://my.f5.com/manage/s/article/K2492" rel="noopener noreferrer"&gt;my.f5&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Session affinity is worth using only when the task actually needs continuity, such as cookie-bound pagination or a multi-step account flow. If requests are independent, long-lived affinity usually makes distribution worse. &lt;a href="https://sites.google.com/view/howproxyprovidersbalanceloadac/home" rel="noopener noreferrer"&gt;sites.google&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Which scheduling algorithm should you use?
&lt;/h2&gt;

&lt;p&gt;For a residential proxy pool, score-based weighted selection is the best general default. &lt;a href="https://proxidize.com/proxy-server/load-balancing/" rel="noopener noreferrer"&gt;proxidize&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Round robin is easy to implement, but it ignores live differences in latency and error rate. Weighted round robin is better when capacity differences are stable, yet it still does not respond to fresh timeout bursts or sudden degradation unless you update weights from recent outcomes. &lt;a href="https://docs.oracle.com/en/operating-systems/oracle-linux/10/balancing/balancing-UsingWeightedRoundRobinLoadBalancingwithHAProxy.html" rel="noopener noreferrer"&gt;docs.oracle&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Use this rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Independent requests: score-based routing.&lt;/li&gt;
&lt;li&gt;Session-bound workflows: score-based routing plus session affinity.&lt;/li&gt;
&lt;li&gt;New pool with little feedback data: weighted round robin as a temporary fallback until enough outcome data exists to score nodes credibly. &lt;a href="https://proxidize.com/proxy-server/load-balancing/" rel="noopener noreferrer"&gt;proxidize&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How should you score each proxy?
&lt;/h2&gt;

&lt;p&gt;A useful health score combines recent success, latency, current load, and cooldown state. If you ignore current load, a proxy can look healthy historically while still being the one you’re about to overload. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-pool-management" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A practical model is:&lt;/p&gt;

&lt;p&gt;[&lt;br&gt;
score = success_factor \times latency_factor \times load_factor \times cooldown_factor \times penalty_factor&lt;br&gt;
]&lt;/p&gt;

&lt;p&gt;Interpret the factors this way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;success_factor&lt;/code&gt;: recent success rate over your rolling window.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;latency_factor&lt;/code&gt;: inverse weight from recent latency, with a cap so a few fast outliers do not dominate.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;load_factor&lt;/code&gt;: penalty based on current &lt;code&gt;in_flight&lt;/code&gt; pressure.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cooldown_factor&lt;/code&gt;: zero when the proxy is cooling down, one when it is eligible again.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;penalty_factor&lt;/code&gt;: extra downgrade after repeated transport failures. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-usage" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For compliant operations, treat failure types differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timeout or connect error: lower the proxy’s score and retry only within your approved retry budget.&lt;/li&gt;
&lt;li&gt;429 from a target host: suspend or sharply reduce traffic to that target and review whether your approved frequency has been exceeded.&lt;/li&gt;
&lt;li&gt;Repeated 403 from a target host: stop the workflow for that target and review authorization, session design, and request necessity before sending more traffic. &lt;a href="https://www.bleepingcomputer.com/news/security/residential-proxies-evaded-ip-reputation-checks-in-78-percent-of-4b-sessions/" rel="noopener noreferrer"&gt;bleepingcomputer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  What provider-specific values do you need before the code will work?
&lt;/h2&gt;

&lt;p&gt;Before you run any scheduler code, collect the provider-specific fields from your proxy vendor’s dashboard or API docs. This is the part many examples skip, and it is where otherwise-correct code often breaks in production. &lt;a href="https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/28899326/3df2e4e2-88fd-4b61-a5fe-0910f71f307c/Gu-Ge-EEATSuan-Fa.md" rel="noopener noreferrer"&gt;ppl-ai-file-upload.s3.amazonaws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Gateway host and port.&lt;/li&gt;
&lt;li&gt;Username and password format.&lt;/li&gt;
&lt;li&gt;How region targeting is encoded.&lt;/li&gt;
&lt;li&gt;Whether the provider supports session affinity, and how that key is passed.&lt;/li&gt;
&lt;li&gt;Whether the provider expects HTTP, HTTPS, or SOCKS5 proxy URLs.&lt;/li&gt;
&lt;li&gt;Any documented plan limits that affect concurrency or session duration.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not guess these values. Proxy vendors often encode region or session parameters differently, and those formats are not interchangeable across providers. &lt;a href="https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/28899326/3df2e4e2-88fd-4b61-a5fe-0910f71f307c/Gu-Ge-EEATSuan-Fa.md" rel="noopener noreferrer"&gt;ppl-ai-file-upload.s3.amazonaws&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How do you implement the scheduler in code?
&lt;/h2&gt;

&lt;p&gt;HTTPX supports async clients and proxy configuration, which makes it a reasonable base for an application-level scheduler in Python. &lt;a href="https://www.python-httpx.org/advanced/proxies/" rel="noopener noreferrer"&gt;python-httpx&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code below is intentionally split into two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Provider-specific settings you must fill from your vendor documentation.&lt;/li&gt;
&lt;li&gt;Policy settings you must fill from your own approved traffic budget and internal rules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That separation is deliberate. It keeps the article actionable without inventing proxy vendor syntax or target-rate values that should come from live docs and approved operating policy. &lt;a href="https://www.python-httpx.org/advanced/proxies/" rel="noopener noreferrer"&gt;python-httpx&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.11+&lt;/li&gt;
&lt;li&gt;&lt;code&gt;httpx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;asyncio&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;httpx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;scheduler.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TargetSuspendedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;required_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing required environment variable: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;required_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RuntimePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;outcome_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;latency_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;max_in_flight_per_proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;timeout_cooldown_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;hard_failure_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;per_proxy_rate_limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;per_proxy_rate_window_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;request_timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;retry_budget&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;target_suspend_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;max_in_flight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;outcome_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;latency_window&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;in_flight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;cooldown_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;hard_failures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;outcome_samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;latency_samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;request_timestamps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__post_init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxlen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxlen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_until&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_in_flight&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;success_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_samples&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;p95_latency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
        &lt;span class="n"&gt;vals&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health_score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;

        &lt;span class="n"&gt;success_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;success_rate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;latency_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;p95_latency&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;load_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_in_flight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;load_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;load_ratio&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;penalty_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;success_factor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;latency_factor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;load_factor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;penalty_factor&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PerProxyRateLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_requests&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;period_seconds&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timestamps&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timestamps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timestamps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;

            &lt;span class="n"&gt;wait_for&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period_seconds&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timestamps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TargetPolicyGate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;suspend_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspend_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;suspend_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspended_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forbidden_counts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;suspended_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspended_until&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;suspended_until&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TargetSuspendedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Traffic to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is suspended until &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strftime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;%Y-%m-%d %H&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="n"&gt;S&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, time.localtime(suspended_until))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forbidden_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_429&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspended_until&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspend_seconds&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_403&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forbidden_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;forbidden_counts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspended_until&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;suspend_seconds&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProxyScheduler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_affinity_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_find_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;choose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session_key&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;session_key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_affinity_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;preferred&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_find_by_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_affinity_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;preferred&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;preferred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;preferred&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;preferred&lt;/span&gt;

        &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;available&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No available proxies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;health_score&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;chosen&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;session_affinity_map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;chosen&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;in_flight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failures&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_transport_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failures&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failures&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cooldown_until&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_with_scheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ProxyScheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rate_limiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PerProxyRateLimiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TargetPolicyGate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RuntimePolicy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_allowed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retry_budget&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;choose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;rate_limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request_timeout_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;latency&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_429&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TargetSuspendedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received 429 from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; target workflow suspended for policy review.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_403&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;TargetSuspendedError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Received 403 from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; target workflow halted pending authorization review.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;attempt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;TargetSuspendedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReadTimeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectTimeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_transport_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout_cooldown_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failure_threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_transport_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;cooldown_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout_cooldown_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hard_failure_threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Request failed after allowed retries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
    &lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;summary&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;latencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;latencies&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95_latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;latencies&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_policy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RuntimePolicy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RuntimePolicy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;outcome_window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OUTCOME_WINDOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;latency_window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LATENCY_WINDOW&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;max_in_flight_per_proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MAX_IN_FLIGHT_PER_PROXY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;timeout_cooldown_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TIMEOUT_COOLDOWN_SECONDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;hard_failure_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HARD_FAILURE_THRESHOLD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;per_proxy_rate_limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PER_PROXY_RATE_LIMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;per_proxy_rate_window_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PER_PROXY_RATE_WINDOW_SECONDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;request_timeout_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REQUEST_TIMEOUT_SECONDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;retry_budget&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RETRY_BUDGET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;target_suspend_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGET_SUSPEND_SECONDS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REPEATED_FORBIDDEN_THRESHOLD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_proxy_nodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RuntimePolicy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;proxy_urls&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nf"&gt;required_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_URL_1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;required_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_URL_2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;required_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_URL_3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy_urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nc"&gt;ProxyNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;max_in_flight&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_in_flight_per_proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;outcome_window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outcome_window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;latency_window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_window&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;nodes&lt;/span&gt;


&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;load_policy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;nodes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_proxy_nodes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ProxyScheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;rate_limiter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PerProxyRateLimiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;max_requests&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per_proxy_rate_limit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;period_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;per_proxy_rate_window_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;target_gate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;TargetPolicyGate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;suspend_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;target_suspend_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;repeated_forbidden_threshold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;test_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required_env&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TARGET_URL&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;total_requests&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TOTAL_REQUESTS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OpsValidation/1.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;limits&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Limits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;required_int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;MAX_CONNECTIONS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_requests&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;session_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="nf"&gt;fetch_with_scheduler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;rate_limiter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;rate_limiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;target_gate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;test_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;session_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_exceptions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SUMMARY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;summarize_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;ERRORS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Required environment variables
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; or deployment secret set with these values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;PROXY_URL_1&lt;/code&gt;, &lt;code&gt;PROXY_URL_2&lt;/code&gt;, &lt;code&gt;PROXY_URL_3&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TARGET_URL&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;OUTCOME_WINDOW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;LATENCY_WINDOW&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAX_IN_FLIGHT_PER_PROXY&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMEOUT_COOLDOWN_SECONDS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;HARD_FAILURE_THRESHOLD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PER_PROXY_RATE_LIMIT&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PER_PROXY_RATE_WINDOW_SECONDS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REQUEST_TIMEOUT_SECONDS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RETRY_BUDGET&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TARGET_SUSPEND_SECONDS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;REPEATED_FORBIDDEN_THRESHOLD&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MAX_CONNECTIONS&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TOTAL_REQUESTS&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Populate proxy URLs from your provider’s live documentation or control panel. Populate rate, retry, cooldown, and suspension settings from your own approved traffic budget and operational policy rather than copying values from another team or vendor marketing page. &lt;a href="https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/28899326/3df2e4e2-88fd-4b61-a5fe-0910f71f307c/Gu-Ge-EEATSuan-Fa.md" rel="noopener noreferrer"&gt;ppl-ai-file-upload.s3.amazonaws&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this implementation is safe to use in production
&lt;/h2&gt;

&lt;p&gt;This version does four things a lab demo usually misses:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It tracks current in-flight pressure, not just past success.&lt;/li&gt;
&lt;li&gt;It enforces per-proxy rate controls.&lt;/li&gt;
&lt;li&gt;It preserves session affinity only when you pass a session key.&lt;/li&gt;
&lt;li&gt;It suspends a target workflow after 429 or repeated 403 instead of treating those responses as a signal to keep routing traffic elsewhere. &lt;a href="https://www.bleepingcomputer.com/news/security/residential-proxies-evaded-ip-reputation-checks-in-78-percent-of-4b-sessions/" rel="noopener noreferrer"&gt;bleepingcomputer&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That last part is the compliance-critical distinction. Transport failures are a proxy-health problem. Repeated 429 and 403 responses are often a target-policy problem, so they should trigger review, not evasive redistribution. &lt;a href="https://www.bleepingcomputer.com/news/security/residential-proxies-evaded-ip-reputation-checks-in-78-percent-of-4b-sessions/" rel="noopener noreferrer"&gt;bleepingcomputer&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How do you verify that balancing actually works?
&lt;/h2&gt;

&lt;p&gt;You verify a residential proxy pool with logs and distribution reports, not intuition. If you can’t see request share, latency, and failure concentration per proxy, you can’t tell whether the scheduler fixed overload or just hid it. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-pool-management" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Record these fields on every request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Timestamp.&lt;/li&gt;
&lt;li&gt;Request ID.&lt;/li&gt;
&lt;li&gt;Target host.&lt;/li&gt;
&lt;li&gt;Selected proxy.&lt;/li&gt;
&lt;li&gt;Session key, if used.&lt;/li&gt;
&lt;li&gt;Attempt number.&lt;/li&gt;
&lt;li&gt;Status code or failure class.&lt;/li&gt;
&lt;li&gt;End-to-end latency.&lt;/li&gt;
&lt;li&gt;Whether the target workflow was suspended. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-usage" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Add this helper if you want a CSV distribution report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;write_distribution_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy_distribution.csv&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;proxy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_latency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latencies&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;avg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lat&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;lat&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
            &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;requests&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;errors&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use these success criteria:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No proxy should sit at saturation while peers remain mostly idle.&lt;/li&gt;
&lt;li&gt;Degraded proxies should lose share naturally because their score drops.&lt;/li&gt;
&lt;li&gt;Session-bound tasks should stay coherent without taking over unrelated traffic.&lt;/li&gt;
&lt;li&gt;If a target triggers suspension logic, the workflow should stop cleanly and visibly rather than silently continuing through other proxies. &lt;a href="https://www.bleepingcomputer.com/news/security/residential-proxies-evaded-ip-reputation-checks-in-78-percent-of-4b-sessions/" rel="noopener noreferrer"&gt;bleepingcomputer&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A healthy output usually looks like a distribution report, not a single “all good” line. You want to see per-proxy request counts and latency values close enough to show balanced use, while still allowing stronger nodes to carry slightly more work than weaker ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What did this revision focus on?
&lt;/h2&gt;

&lt;p&gt;This revision was reviewed on April 20, 2026 against public proxy-pool management guidance, residential proxy usage best practices, and HTTPX proxy documentation. The main conclusion was straightforward: the hard part is rarely the picker by itself; it is the combination of session-affinity scope, per-proxy pressure, and clear stop conditions when a target starts signaling that the workflow needs review. &lt;a href="https://www.python-httpx.org/async/" rel="noopener noreferrer"&gt;python-httpx&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is also where most articles cut corners. They explain rotation, then jump straight to “keep trying.” In production, the better design is to distinguish between proxy-health failures and target-policy signals so your scheduler doesn’t blur the two. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-usage" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why is one proxy still getting overloaded?
&lt;/h2&gt;

&lt;p&gt;The most common reason is that the scheduler is balancing selection count instead of live pressure. A slower proxy can accumulate more open requests even if it is not chosen much more often than its peers. &lt;a href="https://botproxy.com/blog/proxy-server-load-balancing-and-scaling/" rel="noopener noreferrer"&gt;botproxy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Check these patterns:&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: one proxy keeps the most open requests
&lt;/h3&gt;

&lt;p&gt;Cause: the score does not penalize rising &lt;code&gt;in_flight&lt;/code&gt; pressure early enough.&lt;br&gt;&lt;br&gt;
Fix: make &lt;code&gt;load_factor&lt;/code&gt; react sooner and lower &lt;code&gt;MAX_IN_FLIGHT_PER_PROXY&lt;/code&gt; until your logs show stable distribution under your real workload.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: distribution got worse after enabling session affinity
&lt;/h3&gt;

&lt;p&gt;Cause: the session scope is too broad or too long-lived.&lt;br&gt;&lt;br&gt;
Fix: keep session affinity only for workflows that truly depend on continuity, and expire mappings when the task ends. &lt;a href="https://my.f5.com/manage/s/article/K2492" rel="noopener noreferrer"&gt;my.f5&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: a few bad proxies drag down the whole pool
&lt;/h3&gt;

&lt;p&gt;Cause: they never cool down or leave rotation after transport failures.&lt;br&gt;&lt;br&gt;
Fix: send repeated transport failures into cooldown, then let those proxies earn their way back through successful traffic instead of restoring them immediately. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-pool-management" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom: every proxy looks “bad” at once
&lt;/h3&gt;

&lt;p&gt;Cause: the problem may not be the pool at all.&lt;br&gt;&lt;br&gt;
Fix: verify your provider’s gateway syntax, credentials, region parameters, and session-affinity format before changing the scheduler. A malformed proxy URL can look like a balancing failure when it is really an integration bug. &lt;a href="https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/28899326/3df2e4e2-88fd-4b61-a5fe-0910f71f307c/Gu-Ge-EEATSuan-Fa.md" rel="noopener noreferrer"&gt;ppl-ai-file-upload.s3.amazonaws&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build-vs-buy note
&lt;/h2&gt;

&lt;p&gt;If you need custom routing logic, internal observability, and tight control over how tasks are mapped to proxies, building this scheduler layer makes sense. If your team mainly needs reliable residential proxy access without owning the full control plane, evaluating a managed provider is usually the better tradeoff. &lt;a href="https://www.intel471.com/blog/a-look-at-the-residential-proxy-market" rel="noopener noreferrer"&gt;intel471&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That is the natural place to look at Proxy001. Its public site includes residential proxy use-case content and example traffic-based pricing in blog material, which is enough to make it relevant in a build-vs-buy discussion, but you should still confirm the current gateway format, onboarding flow, and any plan-specific details on the live site before integrating. &lt;a href="https://proxy001.com/en-us/blog/practical-guide-to-using-residential-proxies-for-price-monitoring-and-comparison" rel="noopener noreferrer"&gt;proxy001&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance note
&lt;/h2&gt;

&lt;p&gt;Use residential proxies only for permitted tasks such as QA, price monitoring, ad verification, fraud analysis, or data-quality workflows where you have a lawful basis for the traffic. A residential proxy pool is not a substitute for authorization, contract review, or target-site policy review. &lt;a href="https://www.bleepingcomputer.com/news/security/residential-proxies-evaded-ip-reputation-checks-in-78-percent-of-4b-sessions/" rel="noopener noreferrer"&gt;bleepingcomputer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In practice, this means your scheduler should make it easy to stop or reduce traffic when the target indicates pressure or restriction. Good load balancing is not just about throughput; it is also about making policy boundaries visible in the control flow. &lt;a href="https://www.joinmassive.com/en/blog/residential-proxy-usage" rel="noopener noreferrer"&gt;joinmassive&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Go-live checklist
&lt;/h2&gt;

&lt;p&gt;Before rollout, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A per-proxy concurrency cap.&lt;/li&gt;
&lt;li&gt;A per-proxy rate control.&lt;/li&gt;
&lt;li&gt;A documented session-affinity rule.&lt;/li&gt;
&lt;li&gt;Separate handling for transport errors versus target-policy signals.&lt;/li&gt;
&lt;li&gt;Provider-specific gateway and credential syntax verified from live docs.&lt;/li&gt;
&lt;li&gt;Request logs with proxy choice, latency, and outcome.&lt;/li&gt;
&lt;li&gt;A verification report after test traffic.&lt;/li&gt;
&lt;li&gt;A clean suspension path for targets that need policy review. &lt;a href="https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/28899326/3df2e4e2-88fd-4b61-a5fe-0910f71f307c/Gu-Ge-EEATSuan-Fa.md" rel="noopener noreferrer"&gt;ppl-ai-file-upload.s3.amazonaws&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a provider to test this pattern against a real commercial residential proxy offering, Proxy001 is one option worth evaluating. Its site already publishes residential proxy use cases and example traffic-based pricing, which makes it a reasonable starting point for practical assessment. The next step is to confirm the current live signup path, gateway syntax, region parameters, and session-affinity format directly on proxy001.com, then plug those values into the scheduler structure above and validate with your own approved workload. &lt;a href="https://proxy001.com/en-us/blog/top-residential-proxy-providers-worth-considering-in-2026-an-independent-breakdown" rel="noopener noreferrer"&gt;proxy001&lt;/a&gt;&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>networking</category>
      <category>performance</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>Building a Regional Request Router With Residential Proxies in Python</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Thu, 16 Apr 2026 01:53:31 +0000</pubDate>
      <link>https://dev.to/miller_proxy/building-a-regional-request-router-with-residential-proxies-in-python-1afk</link>
      <guid>https://dev.to/miller_proxy/building-a-regional-request-router-with-residential-proxies-in-python-1afk</guid>
      <description>&lt;p&gt;&lt;em&gt;By the Proxy001 engineering team · Last reviewed April 2026&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Disclosure: Proxy001 sponsors this content. All technical specifications are verified from proxy001.com as of April 2026.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;Most tutorials stop at "here's how to pass a proxy dict to &lt;code&gt;requests.get()&lt;/code&gt;." That's fine if you're routing a single request through a single IP. But if you're building a service that sends traffic through a US exit for one task, a DE exit for another, and a JP exit for a third — all from the same Python process — you need a router that maps country codes to proxy sessions and manages them for you.&lt;/p&gt;

&lt;p&gt;This tutorial builds a &lt;code&gt;RegionalRouter&lt;/code&gt; class that does exactly that: maps country codes to residential proxy exits, manages a session pool, handles retries and errors cleanly, and can be wrapped into a lightweight HTTP service with basic access control. You'll have something fully runnable before the end of the page.&lt;/p&gt;


&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you write a single line of router code, confirm the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Python 3.8+&lt;/strong&gt; — f-strings and &lt;code&gt;typing&lt;/code&gt; improvements used throughout&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;requests library&lt;/strong&gt;: &lt;code&gt;pip install requests&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;urllib3 ≥ 1.26.0&lt;/strong&gt; — required for the &lt;code&gt;allowed_methods&lt;/code&gt; parameter on &lt;code&gt;Retry&lt;/code&gt; (ships with &lt;code&gt;requests&lt;/code&gt; 2.26+); earlier versions use the deprecated &lt;code&gt;method_whitelist&lt;/code&gt; instead&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOCKS5 support&lt;/strong&gt; (optional, but recommended): &lt;code&gt;pip install "requests[socks]"&lt;/code&gt; — this pulls in &lt;code&gt;PySocks&lt;/code&gt;, which is needed if your provider offers SOCKS5 endpoints&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;python-dotenv&lt;/strong&gt; for credential management: &lt;code&gt;pip install python-dotenv&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A residential proxy account that supports country-level geo-targeting&lt;/strong&gt; — your provider must allow you to embed a country code in the proxy username. This is the mechanism the entire router relies on. If your provider only offers random rotation without country selection, the geo-routing won't work regardless of how the code is written.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your proxy account should give you four things: a gateway hostname, a port, a username, and a password. Keep those handy.&lt;/p&gt;


&lt;h2&gt;
  
  
  How Does Country-Binding Work in a Residential Proxy?
&lt;/h2&gt;

&lt;p&gt;Most residential proxy providers expose a single gateway — one hostname, one port. Country selection doesn't happen by pointing to a different server; it happens by encoding the target country code directly into the proxy username.&lt;/p&gt;

&lt;p&gt;Bright Data's documentation describes the pattern clearly: to route through a US exit, you append a country flag to your zone username, producing something like &lt;code&gt;customer-XXXX-zone-resi-country-us&lt;/code&gt;. Oxylabs uses the same username-embedding convention with a &lt;code&gt;cc&lt;/code&gt; parameter. The resulting proxy URL takes this form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;http://user-country-US:password@gateway.example.com:10000
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;US&lt;/code&gt; to &lt;code&gt;DE&lt;/code&gt;, &lt;code&gt;JP&lt;/code&gt;, or any ISO 3166-1 alpha-2 code, and the provider routes your request through an exit node in that country. Same gateway, same port — only the username string changes. That's what makes a dynamic router feasible: you're generating per-country proxy URLs at runtime through string interpolation, not maintaining 50 separate connections to 50 different servers.&lt;/p&gt;

&lt;p&gt;The exact username format varies by provider. Check your dashboard's "Proxy Setup" or "Integration" tab and look for a field labeled something like "username with location" or a code example showing country targeting — that string is your template.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1 — Build the Country-to-Proxy URL Mapping
&lt;/h2&gt;

&lt;p&gt;Start by loading your credentials from environment variables. If your password contains characters like &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, or &lt;code&gt;%&lt;/code&gt;, you must URL-encode it before embedding it in the proxy URL — an unescaped &lt;code&gt;@&lt;/code&gt; will break URL parsing and you'll get a &lt;code&gt;407&lt;/code&gt; with no obvious explanation.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PROXY_HOST=gateway.yourprovider.com
PROXY_PORT=10000
PROXY_USER=your_base_username
PROXY_PASS=your_p@ssword!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now build the mapping. The &lt;code&gt;build_proxy_url&lt;/code&gt; function below handles encoding and constructs the country-specific username:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dotenv&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;load_dotenv&lt;/span&gt;

&lt;span class="nf"&gt;load_dotenv&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;PROXY_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PROXY_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PROXY_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PROXY_PASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# encode ALL special chars
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_proxy_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Construct a country-targeted residential proxy URL.
    Country code must be ISO 3166-1 alpha-2 (e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;JP&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;).
    Adjust the username suffix to match your provider&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s exact syntax.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_USER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-country-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="c1"&gt;# Pre-build a mapping for the countries you'll route through
&lt;/span&gt;&lt;span class="n"&gt;SUPPORTED_COUNTRIES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;FR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;COUNTRY_PROXIES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build_proxy_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;build_proxy_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_COUNTRIES&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Provider syntax note:&lt;/strong&gt; The &lt;code&gt;-country-XX&lt;/code&gt; suffix above follows the pattern used by Bright Data and several other major providers. If you're using &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt;, their geo-targeting uses the same username-parameter model — check the "Proxy Setup" section of your dashboard for the exact suffix format for your zone type. Proxy001 supports country, city, and carrier-level targeting across 200+ regions, so the same &lt;code&gt;build_proxy_url&lt;/code&gt; logic applies once you have the correct template string.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2 — Build the &lt;code&gt;RegionalRouter&lt;/code&gt; Class With a Session Pool
&lt;/h2&gt;

&lt;p&gt;Creating a new &lt;code&gt;requests.Session&lt;/code&gt; for every request wastes TCP connection setup time and can exhaust file descriptors under sustained load. The right approach is to pre-build one session per country and reuse it — a session pool.&lt;/p&gt;

&lt;p&gt;One thing to be aware of upfront: pre-building sessions for every supported country means holding that many open connections. For 7–15 countries, the memory cost is negligible. If you're routing through 30+ countries or running in a constrained environment like AWS Lambda, lazy initialization (building sessions on first use rather than at startup) is worth considering — you'd replace the &lt;code&gt;__init__&lt;/code&gt; loop with a &lt;code&gt;_get_or_create_session(cc)&lt;/code&gt; method. For most services, the pre-built approach below is simpler and performs better.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;requests.adapters&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;HTTPAdapter&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib3.util.retry&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Retry&lt;/span&gt;  &lt;span class="c1"&gt;# requires urllib3 &amp;gt;= 1.26.0
&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RoutingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Raised when a regional request cannot be completed.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegionalRouter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;country_proxies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;backoff_factor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        country_proxies: dict mapping ISO country codes to proxy dicts,
                         e.g. {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: {&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}}
        timeout:         (connect_timeout, read_timeout) in seconds
        max_retries:     retry attempts on connection errors and 5xx responses
        backoff_factor:  sleep between retries = backoff_factor * (2 ** (retry_n - 1))
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

        &lt;span class="c1"&gt;# raise_on_status=False here means Retry won't raise mid-retry cycle;
&lt;/span&gt;        &lt;span class="c1"&gt;# the final response's raise_for_status() is called in route() instead,
&lt;/span&gt;        &lt;span class="c1"&gt;# so callers always get a RoutingError with the status code included.
&lt;/span&gt;        &lt;span class="n"&gt;retry_strategy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Retry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;backoff_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;backoff_factor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;status_forcelist&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;allowed_methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GET&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;POST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HEAD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# urllib3 &amp;gt;= 1.26.0
&lt;/span&gt;            &lt;span class="n"&gt;raise_on_status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;adapter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;HTTPAdapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;retry_strategy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;pool_connections&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country_proxies&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# one pool per country
&lt;/span&gt;            &lt;span class="n"&gt;pool_maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxy_dict&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;country_proxies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy_dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        Send a GET request through the residential exit for the given country.

        url:     Target URL
        country: ISO 3166-1 alpha-2 country code (e.g. &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;DE&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;)
        kwargs:  Passed through to session.get() (headers, params, etc.)
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Country &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; is not in the supported list: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sessions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timeout&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProxyError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RoutingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] Proxy connection failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConnectTimeout&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RoutingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] Connection timed out: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RoutingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; from target: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RoutingError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] Request failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire it together with the mapping from Step 1:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegionalRouter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;country_proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;COUNTRY_PROXIES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;Retry&lt;/code&gt; object handles transient 429s and 5xx responses with exponential backoff. Setting &lt;code&gt;pool_connections&lt;/code&gt; to the number of countries gives each country its own connection pool rather than having all sessions compete for a shared one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 3 — Add Graceful Error Handling
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;route()&lt;/code&gt; method wraps all proxy-layer and HTTP-layer exceptions into &lt;code&gt;RoutingError&lt;/code&gt;. Callers don't need to catch five different exception types, the country code is always included in the message, and the original exception is preserved via &lt;code&gt;from e&lt;/code&gt; for stack traces. In practice that last point matters more than it sounds — when a &lt;code&gt;ProxyError&lt;/code&gt; bubbles up in a log aggregator at 2am, having the original exception chained makes the difference between a 5-minute fix and a 30-minute debugging session.&lt;/p&gt;

&lt;p&gt;For your calling code, a typical pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/api/data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Unsupported country — likely a bug in the calling code
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Config error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RoutingError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Proxy or network failure — log and handle gracefully
&lt;/span&gt;    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Routing failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One design decision worth flagging: &lt;code&gt;raise_for_status()&lt;/code&gt; is called inside &lt;code&gt;route()&lt;/code&gt;, which means 4xx and 5xx responses from the &lt;em&gt;target site&lt;/em&gt; also become &lt;code&gt;RoutingError&lt;/code&gt;. That's intentional for most use cases — you want to know if the target returned a 403, not silently process an empty body. If you'd rather inspect the response yourself, remove &lt;code&gt;response.raise_for_status()&lt;/code&gt; from &lt;code&gt;route()&lt;/code&gt; and check &lt;code&gt;response.status_code&lt;/code&gt; in the caller.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do I Confirm the Router Is Using the Right Country Exit?
&lt;/h2&gt;

&lt;p&gt;Run this verification before you point the router at your real targets. It uses &lt;code&gt;ip-api.com/json&lt;/code&gt;, which returns the geographic location of the requesting IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_routing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RegionalRouter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Check that each country exit is actually routing through the target country.
    Prints a pass/fail line per country code.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;countries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://ip-api.com/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;actual_cc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countryCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UNKNOWN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓ PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;actual_cc&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;cc&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✗ FAIL (got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;actual_cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; — exit IP: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RoutingError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  [&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] ✗ ERROR — &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;verify_routing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SUPPORTED_COUNTRIES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  [US] ✓ PASS — exit IP: 104.28.x.x
  [DE] ✓ PASS — exit IP: 185.220.x.x
  [GB] ✓ PASS — exit IP: 51.36.x.x
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once &lt;code&gt;verify_routing()&lt;/code&gt; passes for all target countries, it's worth adding to your CI pipeline or startup health check — catching a misconfigured credential or expired account at deploy time is far better than debugging a geo-mismatch in production at request time.&lt;/p&gt;

&lt;p&gt;That said: if you see a consistent &lt;code&gt;FAIL&lt;/code&gt; for one specific region despite the code looking correct, it's almost always a provider plan limitation rather than a bug. The &lt;code&gt;curl&lt;/code&gt; test in Troubleshooting #3 below confirms this in about 30 seconds, without touching Python at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting: 3 Real Pitfalls
&lt;/h2&gt;

&lt;p&gt;These aren't hypothetical edge cases pulled from documentation. The &lt;code&gt;socks5h&lt;/code&gt; DNS issue is particularly nasty because requests succeed — they just silently exit through the wrong country, or connect to the wrong resolved IP, and nothing in the logs tells you why. The password encoding problem is the most common "it works in curl but not in Python" symptom we've seen. Here's what to look for in each case.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. &lt;code&gt;socks5://&lt;/code&gt; Resolves DNS Locally — Use &lt;code&gt;socks5h://&lt;/code&gt; Instead
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; Requests succeed, but the exit IP doesn't match the target country — or you get connection errors on endpoints that expect hostname-based routing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; With &lt;code&gt;socks5://&lt;/code&gt;, the Python &lt;code&gt;requests&lt;/code&gt; library resolves the hostname on the &lt;em&gt;client side&lt;/em&gt; before forwarding the connection through the proxy. The proxy server sees an IP address, not a hostname. Some residential proxy providers route by hostname at their infrastructure layer, so when they receive a bare IP, the country-targeting logic has nothing to work with.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Switch the proxy URL prefix from &lt;code&gt;socks5://&lt;/code&gt; to &lt;code&gt;socks5h://&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong — DNS resolves locally, country routing may silently fail
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;socks5://user-country-US:pass@gateway.example.com:10000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="c1"&gt;# Correct — DNS resolves on the proxy side
&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;socks5h://user-country-US:pass@gateway.example.com:10000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This requires &lt;code&gt;pip install "requests[socks]"&lt;/code&gt; with &lt;code&gt;PySocks&lt;/code&gt; installed. Update &lt;code&gt;build_proxy_url&lt;/code&gt; to use &lt;code&gt;socks5h://&lt;/code&gt; instead of &lt;code&gt;http://&lt;/code&gt; if your provider's endpoint is SOCKS5.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Special Characters in the Password Cause 407
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;407 Proxy Authentication Required&lt;/code&gt; even though the credentials are confirmed correct in a browser or &lt;code&gt;curl&lt;/code&gt; test.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Characters like &lt;code&gt;@&lt;/code&gt;, &lt;code&gt;:&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;, and &lt;code&gt;%&lt;/code&gt; are URL delimiters. When they appear unencoded in the password portion of a proxy URL, the library misparses where the password ends and the hostname begins. The result is a malformed authentication header that the proxy server rejects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;code&gt;urllib.parse.quote(password, safe="")&lt;/code&gt; encodes every special character, including &lt;code&gt;/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote&lt;/span&gt;

&lt;span class="n"&gt;raw_password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p@ss:w0rd#!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;safe&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Result: "p%40ss%3Aw0rd%23%21"
&lt;/span&gt;
&lt;span class="n"&gt;proxy_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-US:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;encoded&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@gateway.example.com:10000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;build_proxy_url&lt;/code&gt; function in Step 1 already applies this via &lt;code&gt;PROXY_PASS = quote(os.environ["PROXY_PASS"], safe="")&lt;/code&gt;. The risk arises if you construct proxy URLs anywhere else in your codebase without going through that function — worth a quick grep for &lt;code&gt;@{PROXY_HOST}&lt;/code&gt; or &lt;code&gt;@gateway&lt;/code&gt; to catch any direct string construction.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Country Code Is Ignored — Exit IP Doesn't Match
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;verify_routing()&lt;/code&gt; reports &lt;code&gt;FAIL (got XX)&lt;/code&gt; for one or more countries despite the code running without errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cause:&lt;/strong&gt; Not all residential proxy providers support country-level geo-targeting on all plans or pool types. Entry-level plans, shared ISP pools, and some trial accounts route randomly regardless of the username parameter — the provider accepts the credential but silently ignores the country suffix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix — diagnose in three steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rule out the code first.&lt;/strong&gt; Run the equivalent &lt;code&gt;curl&lt;/code&gt; command directly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"http://user-country-DE:pass@gateway:port"&lt;/span&gt; http://ip-api.com/json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;curl&lt;/code&gt; also returns the wrong country, it's a provider or plan issue, not Python.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Check your plan's feature flags.&lt;/strong&gt; Geo-targeting is usually a toggle in the provider dashboard — look for a "Location" or "Targeting" section in your zone settings. It may need to be explicitly enabled.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Confirm the username suffix format.&lt;/strong&gt; Some providers use &lt;code&gt;-country-US&lt;/code&gt;, others use &lt;code&gt;-cc-US&lt;/code&gt;, and some require a different entry-point hostname per country rather than a username parameter. The format in &lt;code&gt;build_proxy_url&lt;/code&gt; may need adjusting. Your provider's integration documentation (not the generic proxy setup page, but the language-specific or zone-specific docs) is the right place to check.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Optional: Expose the Router as a Lightweight HTTP Service
&lt;/h2&gt;

&lt;p&gt;If other services, cron jobs, or CLI tools need to use the router over HTTP, wrapping it in a FastAPI endpoint is straightforward. The snippet below includes a minimal API key check — this matters more than it might seem, because an unauthenticated &lt;code&gt;/fetch&lt;/code&gt; endpoint lets anyone with network access route arbitrary traffic through your proxy quota.&lt;/p&gt;

&lt;p&gt;First, add the API key to your &lt;code&gt;.env&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ROUTER_API_KEY=your-secret-key-here
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Security&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;fastapi.security&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;APIKeyHeader&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;ROUTER_API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROUTER_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;api_key_header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;APIKeyHeader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X-API-Key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;auto_error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_api_key&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Security&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key_header&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;ROUTER_API_KEY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Invalid API key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;FetchRequest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="nd"&gt;@app.post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/fetch&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FetchRequest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Security&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verify_api_key&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;country&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status_code&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;RoutingError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;HTTPException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with: &lt;code&gt;uvicorn main:app --reload&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Call it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8000/fetch &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-API-Key: your-secret-key-here"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"url": "https://example.com", "country": "JP"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For rate limiting before any external exposure, &lt;a href="https://github.com/laurentS/slowapi" rel="noopener noreferrer"&gt;&lt;code&gt;slowapi&lt;/code&gt;&lt;/a&gt; wraps FastAPI in about 10 lines — their README has the decorator-based pattern. Both auth and rate limiting are worth adding before you expose this beyond localhost.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start Routing With a Free Trial
&lt;/h2&gt;

&lt;p&gt;You now have a complete &lt;code&gt;RegionalRouter&lt;/code&gt;: country-to-proxy URL mapping from environment variables, a pre-built session pool with per-country connection pools and exponential-backoff retry, clean error handling with chained exceptions, a &lt;code&gt;verify_routing()&lt;/code&gt; function to confirm geo-exits before deployment, troubleshooting paths for the three most common failures, and an HTTP wrapper with API key authentication.&lt;/p&gt;

&lt;p&gt;The only external dependency is a residential proxy account with geo-targeting enabled. &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; covers 200+ regions with 100M+ IPs and supports country, city, and carrier-level targeting — the same username-parameter model this router is built around. The free trial gives you 500 MB without a credit card, which is enough to run &lt;code&gt;verify_routing()&lt;/code&gt; across your full country list and load-test the session pool before committing to a plan. Pricing runs from $2.00/GB for small volumes down to $0.70/GB at higher tiers (as of April 2026; verify current rates on their &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Disclosure: Proxy001 sponsors this content. Technical specifications verified from proxy001.com as of April 2026.)&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How Residential Proxies Fixed Our Flaky Global E2E Tests</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Mon, 13 Apr 2026 01:43:10 +0000</pubDate>
      <link>https://dev.to/miller_proxy/how-residential-proxies-fixed-our-flaky-global-e2e-tests-3c7b</link>
      <guid>https://dev.to/miller_proxy/how-residential-proxies-fixed-our-flaky-global-e2e-tests-3c7b</guid>
      <description>&lt;p&gt;&lt;em&gt;By the Proxy001 Engineering Team — This post draws from our experience migrating cross-region E2E infrastructure across a distributed test setup targeting multiple production markets.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Your E2E Tests Pass Locally but Fail in CI
&lt;/h2&gt;

&lt;p&gt;The failure looks random — until you check where your CI runner is located.&lt;/p&gt;

&lt;p&gt;We first hit this pattern on a Tuesday: our Frankfurt CI node had been failing the checkout flow at the currency selector step for three days straight. Local runs passed every time. The team spent the better part of two days auditing the component — no code changes, no dependency updates, no meaningful diff between environments. The real cause turned out to be the runner's IP: it was registered to a Hetzner datacenter block, and our CDN was routing it to a UK edge node that served a promotional banner replacing the EUR currency selector with a static campaign element. The DOM node our test was asserting on simply wasn't there from that IP.&lt;/p&gt;

&lt;p&gt;After switching to residential proxies for our EU test traffic, that failure — and a dozen variations of it across other regions — stopped occurring. Pass rate on geo-sensitive flows went from around 70% to consistent green in under a week.&lt;/p&gt;

&lt;p&gt;This is geo-blocking-induced test flakiness, and it's one of the most frustrating kinds because the failure doesn't point at the code. The diff between local and CI is where the HTTP request originates, not what the test does. Residential proxies fix this by routing your test traffic through real home ISP addresses in specific countries, making your test runner look like a real user in that market.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Geo-IP Actually Does to Your Tests
&lt;/h2&gt;

&lt;p&gt;Three separate mechanisms turn tests geo-dependent, and they produce failures that look like race conditions or intermittent network issues but are actually deterministic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;CDN geo-split.&lt;/strong&gt; Major CDNs route requests to region-specific edge nodes that serve different asset bundles, localized content, or entirely different page layouts. A product page might return a UK-specific promotional banner from the London edge node and a different layout from the US node — same code, different IP, different DOM.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Application-side IP detection.&lt;/strong&gt; Many apps parse &lt;code&gt;X-Forwarded-For&lt;/code&gt; or call an IP geolocation API to decide which language, currency, or regulatory compliance flows to render. If your test runner's IP resolves to the wrong country, the app routes it into a flow you never wrote assertions for.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Datacenter IP reputation filtering.&lt;/strong&gt; A significant number of websites — especially those with bot-mitigation systems — automatically flag requests from datacenter IP ranges (AWS, GCP, Azure, Hetzner) and respond with a 403, a CAPTCHA, or a degraded page variant. Your test never reaches the content it's trying to assert on. Same IP range, same failure, every time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Datacenter vs. Residential Proxies: Why It Matters for Test Stability
&lt;/h2&gt;

&lt;p&gt;The core difference is IP reputation and ASN classification.&lt;/p&gt;

&lt;p&gt;Datacenter proxies route through IPs registered to cloud hosting providers. Their ASNs (Autonomous System Numbers) are publicly known and widely blocked at the infrastructure level — they're exactly the traffic pattern that anti-bot systems are built to catch. Residential proxies use addresses issued by consumer ISPs — the same ranges your actual users get. To geo-detection and bot-mitigation systems, they're indistinguishable from organic traffic.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Datacenter Proxy&lt;/th&gt;
&lt;th&gt;Residential Proxy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;IP source&lt;/td&gt;
&lt;td&gt;Cloud hosting ASN&lt;/td&gt;
&lt;td&gt;Consumer ISP ASN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geo-detection accuracy&lt;/td&gt;
&lt;td&gt;Unreliable (commonly flagged as VPN/proxy)&lt;/td&gt;
&lt;td&gt;Accurate (real ISP geo assignment)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot detection risk&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Typical latency&lt;/td&gt;
&lt;td&gt;~5–30ms avg (consistent)&lt;/td&gt;
&lt;td&gt;~20–150ms avg (variable by ISP routing)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best fit&lt;/td&gt;
&lt;td&gt;Non-sensitive scraping, internal tool testing&lt;/td&gt;
&lt;td&gt;Geo-accurate E2E tests, localization QA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For E2E testing where you need to accurately replicate what a real user in Frankfurt, Tokyo, or São Paulo sees, residential proxies are the right tool. The latency increase is real but predictable — you account for it once in your timeout settings rather than debugging phantom failures indefinitely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step: Integrating Residential Proxies into Your Test Framework
&lt;/h2&gt;

&lt;p&gt;The integration path differs meaningfully between frameworks. Playwright gives you the most control — proxy config works at the project and context level. Cypress operates at the process level via environment variables, which creates an important limitation covered below. Selenium requires the most manual handling for authenticated proxies.&lt;/p&gt;

&lt;h3&gt;
  
  
  Playwright
&lt;/h3&gt;

&lt;p&gt;Playwright's native proxy support is the cleanest option for geo-targeted E2E tests. You can set proxy configuration per project in &lt;code&gt;playwright.config.ts&lt;/code&gt;, which lets you run multi-region tests in parallel from a single suite — each project routes through a different country's IP pool. The full proxy API reference is available in &lt;a href="https://playwright.dev/docs/api/class-browsercontext#browser-context-options-proxy" rel="noopener noreferrer"&gt;Playwright's official BrowserContext documentation&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;devices&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;projects&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EU-tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_HOST_EU&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;de-DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;13.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;52.5&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Berlin&lt;/span&gt;
        &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;testMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/eu/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;US-tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;devices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Desktop Chrome&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_HOST_US&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;geolocation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;longitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;87.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;latitude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;41.8&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="c1"&gt;// Chicago&lt;/span&gt;
        &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;geolocation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;testMatch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;**/us/**&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three things worth noting. First, &lt;code&gt;PROXY_HOST_EU&lt;/code&gt; should include the port — typically something like &lt;code&gt;gate.yourprovider.com:7777&lt;/code&gt;. The &lt;code&gt;server&lt;/code&gt; value uses &lt;code&gt;http://&lt;/code&gt; as the scheme even when your target pages are HTTPS; the proxy connection itself is HTTP and it tunnels HTTPS traffic via CONNECT. Second, combining &lt;code&gt;proxy&lt;/code&gt; with &lt;code&gt;locale&lt;/code&gt; and &lt;code&gt;geolocation&lt;/code&gt; gives you the full geo-simulation stack: the proxy sets the outbound IP, the locale affects &lt;code&gt;Accept-Language&lt;/code&gt; headers and &lt;code&gt;Intl&lt;/code&gt; API behavior, and &lt;code&gt;geolocation&lt;/code&gt; handles &lt;code&gt;navigator.geolocation&lt;/code&gt; calls. Third, if any of these tests involve login flows or session state, read the sticky session section before running them.&lt;/p&gt;

&lt;p&gt;To override the proxy for one specific test without touching the project config:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;German pricing page shows EUR&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_HOST_EU&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://yourapp.com/pricing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;locator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-testid="price"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toContainText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;€&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Cypress
&lt;/h3&gt;

&lt;p&gt;Cypress doesn't support per-test or per-project proxy configuration at the network level — it proxies all browser traffic through system environment variables (&lt;a href="https://docs.cypress.io/app/references/proxy-configuration" rel="noopener noreferrer"&gt;official proxy configuration docs&lt;/a&gt;). Set &lt;code&gt;HTTPS_PROXY&lt;/code&gt; before &lt;code&gt;cypress run&lt;/code&gt; and Cypress routes everything through it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;HTTPS_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;http://user:pass@gate.yourprovider.com:7777 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nv"&gt;NO_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost,127.0.0.1 &lt;span class="se"&gt;\&lt;/span&gt;
npx cypress run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;NO_PROXY&lt;/code&gt; value matters. Without it, Cypress's internal communication between the test runner process and the browser also goes through the proxy, causing connection failures.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-region testing in Cypress:&lt;/strong&gt; You can't switch geo within a single &lt;code&gt;cypress run&lt;/code&gt;. The practical workaround is running the suite multiple times with different proxy endpoints — one run per region. Here's a shell wrapper that handles that cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# scripts/run-geo-tests.sh&lt;/span&gt;
&lt;span class="c"&gt;# Runs the full Cypress suite once per region, serially.&lt;/span&gt;
&lt;span class="c"&gt;# Trade-off: 3 regions × 5 min/run = 15 min total wall clock time.&lt;/span&gt;
&lt;span class="c"&gt;# Fine for nightly pipelines; too slow for per-PR runs.&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;REGIONS&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="s2"&gt;"eu"&lt;/span&gt; &lt;span class="s2"&gt;"us"&lt;/span&gt; &lt;span class="s2"&gt;"jp"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;REGION &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGIONS&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"--- Running Cypress suite for region: &lt;/span&gt;&lt;span class="nv"&gt;$REGION&lt;/span&gt;&lt;span class="s2"&gt; ---"&lt;/span&gt;
  &lt;span class="nv"&gt;HTTPS_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"http://&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROXY_USER&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-gate.yourprovider.com:7777"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;NO_PROXY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"localhost,127.0.0.1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  npx cypress run &lt;span class="nt"&gt;--spec&lt;/span&gt; &lt;span class="s2"&gt;"cypress/e2e/**"&lt;/span&gt; &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="nv"&gt;GEO_REGION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;REGION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside your tests, &lt;code&gt;Cypress.env('GEO_REGION')&lt;/code&gt; lets you branch assertions by region if needed. This approach is serial — three regions at five minutes each means fifteen minutes total. Acceptable for a nightly run, painful for per-PR pipelines. If parallel multi-region testing matters for your iteration speed, Playwright's project-based architecture is genuinely the right tool for it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Selenium / WebDriver
&lt;/h3&gt;

&lt;p&gt;For authenticated residential proxies with Selenium, the most reliable approach is &lt;strong&gt;IP whitelisting&lt;/strong&gt;: register your CI runner's outbound IP with your proxy provider, and the proxy grants access without credentials in each request. This sidesteps Chrome's inconsistent handling of inline proxy auth.&lt;/p&gt;

&lt;p&gt;IP-whitelisted setup in Python (cleanest path when available):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test_geo_ipwhitelist.py
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.service&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;

&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="c1"&gt;# PROXY_HOST = gate.yourprovider.com:7777 (IP-whitelisted — no credentials needed)
&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--proxy-server=http://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://yourapp.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If your CI runners have dynamic IPs and can't use whitelisting&lt;/strong&gt;, the standard solution is a Chrome extension that handles &lt;code&gt;onAuthRequired&lt;/code&gt; at the browser level. This is the same pattern used across major proxy providers and is publicly documented:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test_geo_selenium.py — proxy auth via Chrome Manifest V3 extension
&lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;zipfile&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.service&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Service&lt;/span&gt;

&lt;span class="n"&gt;PROXY_HOST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;      &lt;span class="c1"&gt;# e.g., gate.yourprovider.com
&lt;/span&gt;&lt;span class="n"&gt;PROXY_PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="c1"&gt;# e.g., 7777
&lt;/span&gt;&lt;span class="n"&gt;PROXY_USER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;PROXY_PASS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;manifest_json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
{
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Proxy Auth Extension&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manifest_version&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: 3,
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permissions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;storage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webRequest&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;webRequestAuthProvider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;],
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host_permissions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;],
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: { &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service_worker&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background.js&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; },
  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;action&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: { &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default_title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Proxy Auth&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; }
}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;background_js&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
chrome.runtime.onInstalled.addListener(() =&amp;gt; {{
  chrome.proxy.settings.set({{
    value: {{
      mode: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixed_servers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,
      rules: {{
        singleProxy: {{ scheme: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, host: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, port: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; }},
        bypassList: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]
      }}
    }},
    scope: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;regular&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
  }}, () =&amp;gt; {{}});
}});

chrome.webRequest.onAuthRequired.addListener(
  function(details) {{
    return {{ authCredentials: {{ username: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_USER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;, password: &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; }} }};
  }},
  {{ urls: [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;all_urls&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;] }},
  [&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blocking&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]
);
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;plugin_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy_auth_plugin.zip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;zipfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugin_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;zp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;zp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writestr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manifest.json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;manifest_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;zp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writestr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;background.js&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;background_js&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_extension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugin_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--proxy-server=http://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PROXY_PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Service&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://yourapp.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# your test logic here
&lt;/span&gt;&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;_os&lt;/span&gt;
&lt;span class="n"&gt;_os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugin_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# clean up the temp zip
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same &lt;code&gt;manifest.json&lt;/code&gt; + &lt;code&gt;background.js&lt;/code&gt; pattern applies in Java (via AdmZip) and Node.js — if you need those variants, the structure is identical, only the language binding differs. Note that some proxy providers publish a pre-built extension zip you can load directly; check your provider's integration documentation for a download link.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Sticky Sessions Instead of Rotating Proxies
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The short answer: if your test has a login step, use a sticky session. If it doesn't, rotating is fine.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;With a rotating proxy, each new connection can come from a different IP address. Most session management systems — especially those with fraud detection — treat a mid-session IP change as suspicious and invalidate the session token. Your test logs in successfully, then gets bounced back to the login page two clicks later. That looks like a race condition or UI timing issue, but it's the session getting killed by the IP change.&lt;/p&gt;

&lt;p&gt;A sticky session holds one IP for a configurable time window. For E2E tests, set the TTL to cover your longest test flow with a 20% buffer. If your most complex checkout test takes 90 seconds end-to-end, a 2-minute sticky TTL is sufficient. Most residential proxy providers implement sticky sessions through session identifiers in the connection endpoint — the exact format varies by provider, so check your provider's API documentation, but the general pattern looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// playwright.config.ts — sticky session example&lt;/span&gt;
&lt;span class="c1"&gt;// The username format (session suffix) is provider-specific.&lt;/span&gt;
&lt;span class="c1"&gt;// This shows the common pattern; verify the exact syntax in your provider's docs.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sessionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`run_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GITHUB_RUN_ID&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`http://&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_USER&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-session-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;GITHUB_RUN_ID&lt;/code&gt; as part of the session ID ensures each CI run gets a distinct sticky IP rather than colliding with a parallel run.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision rule:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No login, no cart state, no multi-step form → &lt;strong&gt;rotating proxy&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Has login, persistent cart, or any session-dependent flow → &lt;strong&gt;sticky session&lt;/strong&gt;, TTL = longest test duration × 1.2&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Verify Your Proxy Is Actually Working
&lt;/h2&gt;

&lt;p&gt;Before pushing this to CI, confirm geo is working with a dedicated smoke test. It takes under two minutes to run and will save you from chasing a misconfigured setup later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Proxy credentials (or whitelisted IP) from your provider&lt;/li&gt;
&lt;li&gt;Playwright installed (&lt;code&gt;npm init playwright@latest&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Environment variables set locally: &lt;code&gt;PROXY_HOST&lt;/code&gt;, &lt;code&gt;PROXY_USER&lt;/code&gt;, &lt;code&gt;PROXY_PASS&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The geo-check spec:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// tests/geo-check.spec.ts&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;expect&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@playwright/test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;proxy IP resolves to target country&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://ipapi.co/json/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Resolved country: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, IP: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Update 'DE' to your target country code (US, JP, BR, etc.)&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it against your EU project: &lt;code&gt;npx playwright test tests/geo-check.spec.ts --project=EU-tests&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Success criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Test passes without timeout&lt;/li&gt;
&lt;li&gt;Console output shows an IP in your target country (&lt;code&gt;country_code: "DE"&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;The IP shown is not your CI runner's native IP address&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the test passes but &lt;code&gt;country_code&lt;/code&gt; still shows your runner's home country, the proxy is being silently bypassed. The most common cause is a &lt;code&gt;PROXY_HOST&lt;/code&gt; value missing the &lt;code&gt;http://&lt;/code&gt; prefix or an incorrect port number — fix those before debugging anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on scope:&lt;/strong&gt; Routing test traffic through residential proxies is legitimate testing practice, but your test targets still receive real requests. Keep these tests pointed at your own applications or staging environments. Avoid running load-heavy test suites through residential IPs against third-party services — it consumes real users' bandwidth allocations and may violate those services' terms of service. If your tests hit production endpoints you don't own (for localization checks, for example), verify your testing agreement covers automated traffic from external IPs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keeping Proxy Credentials Safe in CI/CD
&lt;/h2&gt;

&lt;p&gt;Never commit proxy credentials to your repository. Store them as CI secrets and inject them as environment variables at runtime — no credential ever touches your codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your repository → Settings → Secrets and variables → Actions → New repository secret.&lt;/li&gt;
&lt;li&gt;Add three secrets: &lt;code&gt;PROXY_HOST&lt;/code&gt;, &lt;code&gt;PROXY_USER&lt;/code&gt;, &lt;code&gt;PROXY_PASS&lt;/code&gt;. (&lt;a href="https://docs.github.com/actions/security-guides/using-secrets-in-github-actions" rel="noopener noreferrer"&gt;GitHub's secrets documentation&lt;/a&gt; covers access controls and secret rotation.)&lt;/li&gt;
&lt;li&gt;Reference them in your workflow:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/e2e.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;E2E Tests&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Playwright browsers&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright install --with-deps chromium&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run E2E tests&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROXY_HOST }}&lt;/span&gt;
          &lt;span class="na"&gt;PROXY_HOST_EU&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROXY_HOST_EU }}&lt;/span&gt;
          &lt;span class="na"&gt;PROXY_HOST_US&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROXY_HOST_US }}&lt;/span&gt;
          &lt;span class="na"&gt;PROXY_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROXY_USER }}&lt;/span&gt;
          &lt;span class="na"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PROXY_PASS }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;GitLab CI:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add the variables under Settings → CI/CD → Variables (mark each as masked). Then reference them in &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .gitlab-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;e2e&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mcr.microsoft.com/playwright:v1.44.0-jammy&lt;/span&gt;
  &lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$PROXY_HOST&lt;/span&gt;
    &lt;span class="na"&gt;PROXY_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$PROXY_USER&lt;/span&gt;
    &lt;span class="na"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$PROXY_PASS&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npx playwright test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add a &lt;code&gt;.env.example&lt;/code&gt; file to your repository with placeholder values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env.example — copy to .env and fill in real values (never commit .env)&lt;/span&gt;
&lt;span class="nv"&gt;PROXY_HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;gate.yourprovider.com:7777
&lt;span class="nv"&gt;PROXY_HOST_US&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;us.yourprovider.com:7777
&lt;span class="nv"&gt;PROXY_HOST_EU&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;eu.yourprovider.com:7777
&lt;span class="nv"&gt;PROXY_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_username
&lt;span class="nv"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The 5 Errors You'll Actually Hit
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. &lt;code&gt;407 Proxy Authentication Required&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Credentials are wrong, or you're providing username/password when the provider expects IP whitelisting — not both. Verify with a manual &lt;code&gt;curl&lt;/code&gt; first: &lt;code&gt;curl -x http://user:pass@host:port https://ipapi.co/json/&lt;/code&gt;. If that fails, check your provider's dashboard for an IP Whitelist section and confirm your CI runner's outbound IP is registered there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. &lt;code&gt;ERR_TUNNEL_CONNECTION_FAILED&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Almost always caused by using &lt;code&gt;https://&lt;/code&gt; in the proxy &lt;code&gt;server&lt;/code&gt; URL. Residential proxies expect &lt;code&gt;http://&lt;/code&gt; for the proxy connection itself, regardless of the target site's protocol — the HTTPS tunneling happens via CONNECT over that HTTP connection. Change &lt;code&gt;server: 'https://gate.yourprovider.com:7777'&lt;/code&gt; to &lt;code&gt;server: 'http://gate.yourprovider.com:7777'&lt;/code&gt; and retry.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. SSL certificate errors (&lt;code&gt;ERR_CERT_AUTHORITY_INVALID&lt;/code&gt;)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proxy is intercepting TLS. For test environments where you're not specifically validating SSL certificates, suppress this per-project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;ignoreHTTPSErrors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scope it only to proxy-targeted projects — don't enable it globally in suites that include SSL validation tests.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Timeout spikes&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Residential proxies route through real consumer ISP connections — average round-trip latency runs 20–150ms, compared to 5–30ms for most datacenter connections. The gap is predictable once you account for it. To find your correct timeout values: run your slowest navigation test without a proxy and note the actual duration from Playwright traces (or use &lt;code&gt;--trace on&lt;/code&gt; for a run). Then set &lt;code&gt;navigationTimeout&lt;/code&gt; to 3× that baseline for proxy-routed projects.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EU-tests&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;actionTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// adjust if your baseline actions exceed ~5s&lt;/span&gt;
    &lt;span class="nx"&gt;navigationTimeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;45&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// 3× a ~15s bare-metal navigation baseline&lt;/span&gt;
    &lt;span class="nx"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;5. IP banned or session drops mid-test&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Unexpected auth redirects or login prompts mid-flow usually mean your sticky session TTL expired before the test finished. Check your provider's dashboard for the maximum TTL available on your current plan — some entry-tier plans cap sticky sessions at 60–120 seconds, which isn't enough for multi-step checkout flows. Either upgrade to a plan with a longer TTL or split long flows into shorter test cases that each complete within the window.&lt;/p&gt;




&lt;h2&gt;
  
  
  Choosing a Residential Proxy Provider for Testing
&lt;/h2&gt;

&lt;p&gt;Generic proxy selection criteria (IP pool size, uptime SLA) don't tell you much for E2E testing specifically. These are the dimensions that actually affect whether your test suite works reliably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Country-level coverage and regional reach&lt;/strong&gt;: Verify the provider has IPs in the specific countries your app serves — not just a headline country count.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky session maximum TTL&lt;/strong&gt;: Confirm the TTL ceiling on the plan you're evaluating, not just "sticky sessions supported." Some plans cap at 1–2 minutes; complex flows need more.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SOCKS5 support&lt;/strong&gt;: Selenium + ChromeDriver integrates more cleanly with SOCKS5 in some configurations. Verify this is available if your stack is Selenium-heavy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrent connection allowance&lt;/strong&gt;: Playwright with 4 workers needs at least 4 simultaneous proxy connections. Add ~50% headroom for retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework integration documentation&lt;/strong&gt;: Providers that publish working code examples for your specific framework — not just generic setup guides — save meaningful setup time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; covers all of these for testing use cases: 100M+ residential IPs across 200+ regions, with geo-targeting options that let you specify the target region for each proxy endpoint — check proxy001.com for the current granularity options. Proxy001 also publishes integration documentation for major testing frameworks — verify current coverage at proxy001.com or your account dashboard after sign-up. They offer a trial option so you can run the geo-check spec above against your actual test targets before committing to a plan; visit proxy001.com to check current plans and sign-up terms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Run Your Next Geo Test With Confidence
&lt;/h2&gt;

&lt;p&gt;If you've been losing hours to false-negative CI failures caused by geo-blocking and datacenter IP reputation filters, the fix doesn't require framework changes or test rewrites — just routing your test traffic through IPs that match your users' actual locations.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; makes that practical without enterprise overhead. Their 100M+ residential IP pool spans 200+ regions with real ISP assignments, supports both rotating and sticky sessions for login-heavy flows, and the integration snippets in this article are ready to use with their endpoints today.&lt;/p&gt;

&lt;p&gt;Visit &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;proxy001.com&lt;/a&gt; to explore plans and sign-up options. Drop your credentials into the &lt;code&gt;playwright.config.ts&lt;/code&gt; template above, run the geo-check spec, and confirm the &lt;code&gt;country_code&lt;/code&gt; assertion passes. If it does, your geo-blocking flakiness problem is solved.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Setting Up a Multi-Region Google SEO Rank Tracking System Using Residential Proxy IPs</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Wed, 08 Apr 2026 01:57:31 +0000</pubDate>
      <link>https://dev.to/miller_proxy/setting-up-a-multi-region-google-seo-rank-tracking-system-using-residential-proxy-ips-nh9</link>
      <guid>https://dev.to/miller_proxy/setting-up-a-multi-region-google-seo-rank-tracking-system-using-residential-proxy-ips-nh9</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; This article is produced by Proxy001's content team. Proxy001 is mentioned once in the Prerequisites section as a recommended provider based on verified product features. We recommend evaluating providers based on your specific regional requirements before committing to any plan.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;Residential proxy IPs combined with a rank tracking tool can get you genuinely accurate, region-specific Google ranking data — this is standard practice for agencies and in-house teams managing SEO across multiple markets. Whether you're monitoring a US retail brand's city-level pack positions or tracking keyword movement across five countries simultaneously, the underlying architecture is the same.&lt;/p&gt;

&lt;p&gt;The gap between "bought proxies" and "working system" is larger than most guides let on. Without the right geo-targeting configuration, IP rotation strategy, and request pacing, you'll either collect inaccurate data — because Google served results based on the proxy's ASN rather than the claimed region — or trigger rate protection that corrupts your dataset. This guide covers everything needed to build this system end to end: prerequisites, architecture, full code with SERP parsing, SEO PowerSuite setup, multi-region scaling, data verification, troubleshooting, and a realistic compliance assessment.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Do You Need Before You Start?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Pick your tracking path first
&lt;/h3&gt;

&lt;p&gt;There are two practical approaches. Commit to one before configuring anything — they have different proxy requirements and different maintenance burdens.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path A — SEO PowerSuite Rank Tracker (GUI):&lt;/strong&gt; Best if you need scheduling, a built-in reporting UI, and don't want to maintain code. Proxy rotation is built in, configuration takes under 10 minutes. Covered in full below.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path B — Custom Python script:&lt;/strong&gt; Best for teams that need programmatic control over keyword sets, output format, and multi-region parallelization. More setup overhead upfront, more flexibility once running. The full implementation — including SERP parsing and rank extraction — is in the integration section.&lt;/p&gt;

&lt;p&gt;Both paths require a residential proxy account with geo-targeting support. If you're already using a commercial SERP API like DataForSEO or SerpAPI, those services handle geo-targeting and parsing internally; you don't need proxies at all. This guide is specifically for teams running their own tracking infrastructure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get a residential proxy account with geo-targeting support
&lt;/h3&gt;

&lt;p&gt;Not every residential proxy service handles country- and city-level targeting with enough precision for rank tracking. You need a provider that supports geo-targeting at minimum to the country level (city-level for local SEO use cases), offers both rotating and sticky session modes, has meaningful IP pool depth in every region you're tracking, and explicitly supports SEO monitoring workloads.&lt;/p&gt;

&lt;p&gt;For this setup, &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; covers all of the above: 100M+ residential IPs across 200+ regions, country and city-level targeting, both rotating and static IP modes, and documented integration examples for Python, Scrapy, and Selenium. Their free trial is the most reliable way to validate IP pool depth in your specific target regions before committing to a plan — pool depth for city-level targeting varies significantly across providers and secondary markets, and you want to know about gaps before they appear as production data gaps.&lt;/p&gt;

&lt;h3&gt;
  
  
  Estimate your bandwidth before signing up
&lt;/h3&gt;

&lt;p&gt;Residential proxies are billed by bandwidth, and multi-region rank tracking consumes more than most people expect. The HTML response for a Google SERP query — what your proxy request actually downloads — is the raw HTML document, not a fully rendered page with all assets. Plain informational SERPs typically run in the 20–40 KB range; rich SERPs with multiple ad blocks, featured snippets, and People Also Ask sections can push toward 60–80 KB.&lt;/p&gt;

&lt;p&gt;You can measure your own target SERP sizes in under 5 minutes before committing to a bandwidth plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Measure actual HTML response size for a sample keyword&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"Size: %{size_download} bytes&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://www.google.com/search?q=seo+proxy+service&amp;amp;num=10"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run this against 10–15 representative keywords from your tracking set and use the average for your estimate. Then apply this formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Monthly bandwidth (GB) = keywords × regions × daily_checks × avg_KB × 30 ÷ 1,000,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt; 500 keywords × 5 regions × 1 daily check × 40 KB × 30 ÷ 1,000,000 = &lt;strong&gt;3 GB/month&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add 20–30% on top for retries and proxy verification requests. For city-level tracking (e.g., 10 cities per country instead of 1 national endpoint), multiply region count accordingly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lock in your region list before configuring anything
&lt;/h3&gt;

&lt;p&gt;Define target countries and cities in advance and store them in a version-controlled config file. Switching from national to city-level targeting mid-run means reconfiguring every proxy endpoint and invalidating historical comparisons. The config structure is covered in the scaling section below.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does Multi-Region Rank Tracking Actually Work?
&lt;/h2&gt;

&lt;p&gt;The system runs as a five-stage pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your server / local machine
    ↓
Rank tracking tool or Python script
    ↓
Residential proxy endpoint (geo-targeted to target region)
    ↓
Google.com — sees an ISP-registered IP from the target region
    ↓
Region-specific SERP → parse rank → store with keyword + region + timestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical stage is what happens at Google. Organic rankings, local packs, and featured snippets all vary based on the requester's geographic location. Google determines that location by checking the requesting IP against its IP intelligence database, which maps addresses to their registered ISP and geography.&lt;/p&gt;

&lt;p&gt;This is why datacenter proxies fail here. Datacenter IPs belong to ASNs registered to cloud providers — AWS, DigitalOcean, Vultr — and Google's systems recognize these ASNs. The result is either generic non-geo-personalized SERP results, a rate protection response, or an outright block. Residential IPs are registered to consumer ISPs (Comcast, BT, Deutsche Telekom) and carry the same trust profile as a real user's home connection. Proxyway's 2025 proxy market benchmark measured a median 94.3% success rate for residential proxies against Google specifically, compared to significantly lower rates for datacenter IPs. &lt;a href="https://proxyway.com/research/proxy-market-research-2025" rel="noopener noreferrer"&gt;proxyway&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;gl&lt;/code&gt; and &lt;code&gt;hl&lt;/code&gt; URL parameters Google exposes for geo-targeting are a useful reinforcement layer when aligned with the proxy's IP location. Community-reported testing consistently shows better locale consistency when both the IP and URL parameters point at the same region — use both together as a safe default.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Set Up Geo-Targeting for Each Region?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Proxy endpoint formats
&lt;/h3&gt;

&lt;p&gt;Most residential proxy providers expose geo-targeting through credential encoding. Two common patterns:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Username-embedded geo:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;http://username-country-us-city-chicago:password@gate.provider.com:port
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Country code shorthand:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;http://username-cc-us:password@gate.provider.com:port
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact format is provider-specific — check your provider's integration documentation for the credential syntax they support. Getting it wrong produces silently inaccurate geo-targeting: the request succeeds but the IP exits from the wrong region. The verification step in the Python code below catches this before it contaminates a full tracking run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Country-level vs. city-level targeting
&lt;/h3&gt;

&lt;p&gt;Country-level targeting is sufficient for national SERPs where Google's results don't vary significantly within a country. City-level becomes necessary for local pack rankings, "near me" queries, or any keyword where Google's local algorithm produces meaningfully different results by metro area — "emergency plumber" in Chicago and Houston return completely different results; a US national proxy gives you neither.&lt;/p&gt;

&lt;p&gt;One practical consideration: IP pool depth for city-level targeting in secondary markets is documented by providers as notably smaller than for major US metropolitan areas. Ask your provider specifically about pool depth for each target city before configuring volume tracking at that granularity. Thinner pools mean each IP in the pool handles more requests, which increases per-IP exposure. For city-level tracking in secondary markets, keep request volume modest or spread jobs over longer time windows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rotating vs. sticky sessions: the actual decision logic
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Use rotating proxies for production bulk runs.&lt;/strong&gt; When running your full keyword set across all regions — say, a daily 1,000-keyword check — rotating mode distributes requests across the IP pool automatically. This keeps per-IP request counts low, which is the most reliable way to keep legitimate monitoring from triggering rate protection systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use sticky sessions for spot-checks and troubleshooting.&lt;/strong&gt; Rotating proxies can produce variance in repeated checks because different IPs within the same country pool may route through different regional sub-clusters, each serving slightly different local SERP compositions. What looks like a 2–3 position ranking change can actually be different IPs resolving to different Google edge nodes. Sticky sessions eliminate this variable when you need a reproducible result from a defined geographic point. Sticky sessions typically hold the same IP for 10–30 minutes depending on provider configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recommended operating mode:&lt;/strong&gt; rotating proxies for all scheduled production runs, sticky session endpoints available for manual verification and anomaly investigation.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Connect the Proxy to Your Rank Tracker?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Path A: SEO PowerSuite Rank Tracker (GUI)
&lt;/h3&gt;

&lt;p&gt;SEO PowerSuite's Rank Tracker has native proxy rotation support with direct credential entry. Steps from their official documentation: &lt;a href="https://www.link-assistant.com/help/rank-tracker/search-safety-settings.html" rel="noopener noreferrer"&gt;link-assistant&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Rank Tracker → &lt;strong&gt;Preferences → Search Safety Settings&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Proxy Rotation…&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Check &lt;strong&gt;Enable proxy rotation&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add&lt;/strong&gt; to enter proxy servers individually:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Address:&lt;/strong&gt; your proxy hostname&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Port:&lt;/strong&gt; the assigned port number&lt;/li&gt;
&lt;li&gt;If using username/password auth: check &lt;strong&gt;Proxy requires authentication&lt;/strong&gt; and enter credentials&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;To add in bulk: click &lt;strong&gt;Import&lt;/strong&gt; and paste entries in &lt;code&gt;hostname:port&lt;/code&gt; format, one per line&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;Number of Simultaneous Tasks&lt;/strong&gt; to roughly &lt;strong&gt;one-third your proxy count&lt;/strong&gt; — this ensures each active task has backup proxies available &lt;a href="https://www.link-assistant.com/help/rank-tracker/search-safety-settings.html" rel="noopener noreferrer"&gt;link-assistant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disable&lt;/strong&gt; "Look for new proxies" — this option searches for public proxies, which you don't want when using private endpoints &lt;a href="https://www.link-assistant.com/help/rank-tracker/search-safety-settings.html" rel="noopener noreferrer"&gt;link-assistant&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;For each geo-targeted region, add the corresponding regional endpoint from your provider&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Verifying the proxy is actually working in Rank Tracker:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After completing configuration, run a manual single-keyword check before kicking off a full campaign:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Go to &lt;strong&gt;Preferences → Search Safety Settings → Proxy Rotation&lt;/strong&gt; and click &lt;strong&gt;Check&lt;/strong&gt; next to each proxy — Rank Tracker will test connectivity and display status (Alive / Dead) and response time&lt;/li&gt;
&lt;li&gt;Run one keyword manually (right-click → Check Rankings) and open &lt;strong&gt;Logs&lt;/strong&gt; from the bottom panel — successful proxy usage shows requests routed through the proxy host address rather than your direct IP&lt;/li&gt;
&lt;li&gt;As a geo-accuracy sanity check: run a keyword you know has strong regional signal (e.g., a local business category) and verify the SERP results contain region-appropriate content. If a UK-targeted proxy returns predominantly US results, the geo-targeting configuration needs review&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Path B: Custom Python tracker
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Python 3.8+, &lt;code&gt;requests&lt;/code&gt;, &lt;code&gt;beautifulsoup4&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;requests beautifulsoup4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Step 1: Verify proxy geo-targeting before any data collection&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run this against every proxy endpoint before a tracking session. A misconfigured geo parameter produces silent data errors — this check takes under 5 seconds and catches the problem before it contaminates a full run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_proxy_location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Confirm a proxy endpoint routes through the expected geographic region.
    proxy_url format: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;http://username:password@gate.provider.com:port&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;
    Returns dict with ip, country, city, isp fields.
    Note: ip-api.com free tier allows 45 requests/minute — sufficient for
    pre-session verification of a small proxy set.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;proxies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://ip-api.com/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;query&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;country&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;countryCode&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;city&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="c1"&gt;# Example usage
&lt;/span&gt;&lt;span class="n"&gt;proxy_us&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://username-country-us:password@gate.proxy001.com:7777&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;verify_proxy_location&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;proxy_us&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Expected: {'ip': '...', 'country': 'US', 'city': 'Chicago', 'isp': 'Comcast Cable Communications'}
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;country&lt;/code&gt; doesn't match your target, stop and fix the geo-targeting configuration before proceeding.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Step 2: Fetch a geo-targeted Google SERP&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Both the proxy IP and the &lt;code&gt;gl&lt;/code&gt;/&lt;code&gt;hl&lt;/code&gt; URL parameters should point at the same region. The &lt;code&gt;Accept-Language&lt;/code&gt; header must also match — a mismatch between the header and &lt;code&gt;hl&lt;/code&gt; parameter can cause Google to return content in the wrong language. &lt;a href="https://dev.to/mateuszbuda/scraping-google-serp-with-geolocation-4f1m"&gt;dev&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;quote_plus&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fetch_google_serp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     &lt;span class="c1"&gt;# ISO 3166-1 alpha-2, e.g. "us", "gb", "de"
&lt;/span&gt;    &lt;span class="n"&gt;language_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# e.g. "en", "de", "fr"
&lt;/span&gt;    &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;num_results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Fetch a geo-targeted Google SERP through a residential proxy.
    Returns Response on success (HTTP 200, no rate-limit redirect), None on failure.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;encoded_kw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quote_plus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.google.com/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?q=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;encoded_kw&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;gl=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;hl=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;language_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;num=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;num_results&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 (Windows NT 10.0; Win64; x64) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;AppleWebKit/537.36 (KHTML, like Gecko) &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/124.0.0.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;language_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;language_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;;q=0.9,en;q=0.8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Encoding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gzip, deflate, br&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;proxies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sorry/index&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exceptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Step 3: Parse SERP HTML and extract rank&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the step most tutorials skip. The proxy connection alone doesn't give you ranking data — you need to extract the position of your target domain from the returned HTML.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Google's HTML structure changes periodically. The selectors below reflect Google's markup as of April 2026, based on community-documented SERP structure analysis.  If &lt;code&gt;extract_rank()&lt;/code&gt; unexpectedly returns &lt;code&gt;None&lt;/code&gt; for keywords you know rank, run &lt;code&gt;debug_serp_structure()&lt;/code&gt; to inspect the current element layout and update the selectors. &lt;a href="https://blog.scrapeup.com/scraping-google-search-2026/" rel="noopener noreferrer"&gt;blog.scrapeup&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;urllib.parse&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urlparse&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Parse Google SERP HTML to find the organic rank of target_domain.

    target_domain: domain to search for, e.g. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;
                   (without scheme; www. prefix is normalized automatically)
    Returns: 1-based rank position (int), or None if not found in results.

    Primary container selector: div.Ww4FFb (organic result wrapper, April 2026)
    Update this selector if debug_serp_structure() shows 0 containers.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;target_clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target_domain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;www.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;organic_containers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;div.Ww4FFb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;organic_containers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a[href]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;link&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;parsed_domain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;href&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;www.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target_clean&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;parsed_domain&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# target not found in the result set
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;debug_serp_structure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Print the first N organic result URLs from a SERP response.
    Use this when extract_rank() returns unexpected None values to verify
    that selectors are still matching Google&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s current HTML layout.
    If 0 containers are found, Google has changed its structure —
    inspect raw HTML and update the selector in extract_rank().
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;containers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;div.Ww4FFb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Found &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; organic result containers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;containers&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a[href]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Result &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;href&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;no link found&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Step 4: Scale to multiple regions in parallel&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The threading model below assigns exactly one thread per region and processes that region's keywords sequentially within that thread. This is important: a flat task pool with &lt;code&gt;max_workers=len(regions)&lt;/code&gt; doesn't prevent the scheduler from running multiple keywords from the same region concurrently, which can stack requests through the same proxy endpoint and defeat the per-request delay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;

&lt;span class="c1"&gt;# Region config — keep this in a version-controlled YAML file in production
&lt;/span&gt;&lt;span class="n"&gt;REGION_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-us:pass@gate.proxy001.com:7777&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-gb:pass@gate.proxy001.com:7777&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;de&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-de:pass@gate.proxy001.com:7777&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;de&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;de&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seo proxy service&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;residential proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;best proxy providers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;TARGET_DOMAIN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;yoursite.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_region&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;region_code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Process all keywords for a single region sequentially.
    Sequential processing within a region ensures the per-request delay
    applies properly and requests don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t stack on one proxy endpoint.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;REGION_CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;region_code&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetch_google_serp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;country_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;language_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TARGET_DOMAIN&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;region_code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rank&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_multi_region_tracking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Run rank tracking across all configured regions in parallel.
    One thread per region; keywords processed sequentially within each thread.
    Regions run concurrently with each other — total runtime ≈ single-region runtime.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;all_results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;REGION_CONFIG&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;future_to_region&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;process_region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;region&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;REGION_CONFIG&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future_to_region&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;all_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;all_results&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;run_multi_region_tracking&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;rank&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not ranked / request failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;region&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; → &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Request pacing: responsible configuration for legitimate monitoring
&lt;/h3&gt;

&lt;p&gt;Google's automated rate protection activates on patterns that match bulk automated querying — the same mechanism that blocks abusive bot traffic can flag legitimate rank monitoring if configured too aggressively. Community-reported benchmarks and proxy provider documentation consistently identify similar thresholds for legitimate monitoring workflows.  A key variable is predictability: a fixed interval carries a stronger automation signal than the same average rate with randomized timing, even if the requests-per-minute is identical. &lt;a href="https://docs.google.com/document/d/1Ax2csCOdOYN6R0tVMyi1Zko2PsH3I_oR5NFsuDCCp0Y/mobilebasic" rel="noopener noreferrer"&gt;docs.google&lt;/a&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Request pattern&lt;/th&gt;
&lt;th&gt;Risk of triggering rate protection&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&amp;gt; 1 request/second from same IP&lt;/td&gt;
&lt;td&gt;Rate protection activates within minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed 3–5 second interval, same IP&lt;/td&gt;
&lt;td&gt;Matches automated patterns; elevated false-positive risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fixed 8 second interval, same IP&lt;/td&gt;
&lt;td&gt;Reduced rate, but fixed interval remains a bot pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Randomized 8–15 seconds per request, same IP&lt;/td&gt;
&lt;td&gt;Consistent with browsing patterns; low false-positive risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full IP rotation (new IP per request)&lt;/td&gt;
&lt;td&gt;Distributes load across IP pool; lowest per-IP exposure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The randomization is what matters most. This keeps per-IP request frequency within the range that legitimate SEO monitoring consistently operates without triggering rate protection, while also avoiding the fixed-interval timing signature that automated systems recognize. &lt;a href="https://wpseoai.com/blog/is-web-scraping-against-google/" rel="noopener noreferrer"&gt;wpseoai&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Track Rankings Across 5+ Regions Without Chaos?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use a config file, not hardcoded endpoints
&lt;/h3&gt;

&lt;p&gt;Store region list, proxy credentials, and tracking parameters in a YAML file under version control. When a proxy credential rotates — which it will — you update one file, not the script. When you add a region, same process.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# regions.yaml&lt;/span&gt;
&lt;span class="na"&gt;regions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-us:pass@gate.proxy001.com:7777"&lt;/span&gt;
    &lt;span class="na"&gt;gl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;us&lt;/span&gt;
    &lt;span class="na"&gt;hl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
    &lt;span class="na"&gt;check_frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gb&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-gb:pass@gate.proxy001.com:7777"&lt;/span&gt;
    &lt;span class="na"&gt;gl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gb&lt;/span&gt;
    &lt;span class="na"&gt;hl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;en&lt;/span&gt;
    &lt;span class="na"&gt;check_frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;daily&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-country-de:pass@gate.proxy001.com:7777"&lt;/span&gt;
    &lt;span class="na"&gt;gl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt;
    &lt;span class="na"&gt;hl&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;de&lt;/span&gt;
    &lt;span class="na"&gt;check_frequency&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weekly&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Store results with three mandatory fields
&lt;/h3&gt;

&lt;p&gt;Every tracking record needs &lt;code&gt;keyword&lt;/code&gt;, &lt;code&gt;region&lt;/code&gt;, and &lt;code&gt;timestamp&lt;/code&gt;. Without all three, your data becomes ambiguous within 48 hours. If writing to a database, add a composite index on &lt;code&gt;(keyword, region, timestamp)&lt;/code&gt; for efficient trend queries.&lt;/p&gt;

&lt;h3&gt;
  
  
  Differentiated check frequency saves bandwidth
&lt;/h3&gt;

&lt;p&gt;Daily for core markets, weekly for secondary ones. Dropping two of five regions from daily to weekly cuts monthly requests by 40% at the same keyword set size. The &lt;code&gt;check_frequency&lt;/code&gt; field in the YAML above is the implementation point — add a simple filter in your scheduling logic before calling &lt;code&gt;run_multi_region_tracking()&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do You Know the Ranking Data Is Actually Region-Accurate?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Verify IP location before every session.&lt;/strong&gt; Run &lt;code&gt;verify_proxy_location()&lt;/code&gt; against each proxy endpoint before starting a full tracking session. A misconfigured endpoint passes the connection test while quietly routing through the wrong region — catching it here is much cheaper than catching it in your data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check SERP language consistency.&lt;/strong&gt; After fetching a SERP, verify the HTML contains content in the expected language. A &lt;code&gt;hl=de&lt;/code&gt; request returning predominantly English results is a reliable indicator of geo-targeting misconfiguration, not a ranking problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run a sticky-session consistency check.&lt;/strong&gt; For a sample of 10–20 keywords per region, make three requests using sticky session endpoints within a 10-minute window. Rankings should be within ±1–2 positions. Larger variance suggests IP geo-instability — the same country-targeted endpoint may be cycling through IPs from different sub-regions on each request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Path A users (SEO PowerSuite):&lt;/strong&gt; after a full tracking run completes, open Logs and spot-check 3–5 entries to confirm requests went through proxy hosts rather than your direct IP. Additionally, compare a handful of results against what you'd see browsing via a VPN from the same country — meaningful divergence is a signal to recheck your geo-targeting endpoint configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know your baseline success rate.&lt;/strong&gt; For high-quality residential proxies against Google, expect 90–95% request success. Proxyway's 2025 research measured a median 94.3% for Google-specific targets.  If your rate drops below 85%, investigate before scaling — common causes include IP pool exhaustion in a specific region, pacing that's too aggressive, or expired credentials. &lt;a href="https://proxyway.com/research/proxy-market-research-2025" rel="noopener noreferrer"&gt;proxyway&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Most likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rate protection responses (HTTP 429 or redirect to sorry/index)&lt;/td&gt;
&lt;td&gt;Per-IP request frequency above threshold&lt;/td&gt;
&lt;td&gt;Increase per-request delay to 15–30s; switch to full rotating mode to distribute load across IP pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rankings inconsistent across repeated runs for same keyword&lt;/td&gt;
&lt;td&gt;Geo-targeting drifting — proxy returning IPs from different sub-regions&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;verify_proxy_location()&lt;/code&gt; mid-session; check if provider is cycling geography alongside IP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One specific region has &amp;gt; 20% failure rate&lt;/td&gt;
&lt;td&gt;That region's IP pool is thin or exhausted&lt;/td&gt;
&lt;td&gt;Contact provider about pool depth for that location; fall back from city-level to country-level targeting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;extract_rank()&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt; for keywords you know rank&lt;/td&gt;
&lt;td&gt;Google changed SERP HTML structure; selectors are stale&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;debug_serp_structure()&lt;/code&gt; on a live response; identify new organic result container class; update selector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Response times consistently &amp;gt; 8 seconds&lt;/td&gt;
&lt;td&gt;Proxy exit node is geographically far from target Google data center&lt;/td&gt;
&lt;td&gt;Ask provider about routing options for that region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SERP returns results in wrong language&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Accept-Language&lt;/code&gt; header and &lt;code&gt;hl&lt;/code&gt; parameter mismatch&lt;/td&gt;
&lt;td&gt;Set both explicitly per region in &lt;code&gt;fetch_google_serp()&lt;/code&gt; config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All regions stopped working simultaneously&lt;/td&gt;
&lt;td&gt;Bandwidth cap hit or credentials expired&lt;/td&gt;
&lt;td&gt;Check provider dashboard immediately; verify monthly usage against your plan limit&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Is This Legal? What Are the Real Risks?
&lt;/h2&gt;

&lt;p&gt;You need a realistic assessment, not reassurance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google's Terms of Service position is clear.&lt;/strong&gt; Google's ToS explicitly prohibit using automated means to access or use their services.  Automated rank tracking queries fall within that prohibition — there's no interpretation where this is ToS-compliant. &lt;a href="https://policies.google.com/terms?hl=en-US" rel="noopener noreferrer"&gt;policies.google&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What that means in practice.&lt;/strong&gt; Violating Google's ToS is a platform compliance issue, not a criminal matter. Google's enforcement tools are technical: IP blocks, rate protection responses, and in theory, restrictions on authenticated accounts. The legal landscape around automated access to publicly accessible data is genuinely contested — the 9th Circuit's ruling in &lt;em&gt;hiQ v. LinkedIn&lt;/em&gt; held that automated access to publicly available data doesn't automatically constitute unauthorized computer access, though that ruling is specific to its facts and ongoing litigation across circuits has produced mixed outcomes.  Rank tracking occupies a gray zone: it queries publicly accessible results but at automated scale in violation of platform terms. No documented enforcement action has targeted standard rank monitoring operations. &lt;a href="https://www.eff.org/cases/hiq-v-linkedin" rel="noopener noreferrer"&gt;eff&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk tiers in plain terms:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Risk&lt;/th&gt;
&lt;th&gt;Probability&lt;/th&gt;
&lt;th&gt;Context&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Proxy IP triggers rate protection&lt;/td&gt;
&lt;td&gt;High without rotation; low with residential IP rotation&lt;/td&gt;
&lt;td&gt;This is what proxy rotation is designed to manage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google account restricted&lt;/td&gt;
&lt;td&gt;Low — only relevant if querying while logged in&lt;/td&gt;
&lt;td&gt;Don't run rank tracking through authenticated sessions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Civil legal action from Google&lt;/td&gt;
&lt;td&gt;Very low&lt;/td&gt;
&lt;td&gt;No documented precedent against standard monitoring operations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Criminal liability&lt;/td&gt;
&lt;td&gt;Essentially zero&lt;/td&gt;
&lt;td&gt;Not applicable to this use case&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The industry context:&lt;/strong&gt; Essentially every major SEO platform — Semrush, Ahrefs, Moz — runs large-scale data collection infrastructure that queries Google at scale. Rank monitoring via residential proxies is an accepted industry practice. Operating responsibly means controlling request rates, not overloading Google's infrastructure, and using collected data for your own analysis rather than commercially redistributing raw SERP data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The ToS-compliant alternative:&lt;/strong&gt; Google's Custom Search JSON API provides official programmatic search access. The free tier allows 100 queries/day; paid tiers scale further. It's not designed for rank tracking at production volume, but it's the right choice for organizations where ToS compliance is a hard requirement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Start Tracking With a Free Trial
&lt;/h2&gt;

&lt;p&gt;Start scoped: one region, 50 keywords, daily checks for a week. Use &lt;code&gt;debug_serp_structure()&lt;/code&gt; to confirm your SERP parsing is working, spot-check 5–10 results against a VPN session from the same country, and verify your success rate is above 90% before scaling.&lt;/p&gt;

&lt;p&gt;Proxy001 is built for this kind of seo proxy infrastructure. Their 100M+ residential IP pool spans 200+ regions with country and city-level geo-targeting — covering national SERPs and local pack tracking — alongside rotating and static IP modes for the mixed workflow this system requires. The Python, Scrapy, and Selenium integration documentation maps directly to the code in this guide. Their free trial lets you validate geo coverage and pool depth in your specific target markets before committing to a paid plan — particularly useful for secondary markets where depth varies significantly across providers. Start at &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;proxy001.com&lt;/a&gt;.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How Rotating Residential Proxies Handle IP Assignment and Session Management Under the Hood</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Thu, 02 Apr 2026 03:16:55 +0000</pubDate>
      <link>https://dev.to/miller_proxy/how-rotating-residential-proxies-handle-ip-assignment-and-session-management-under-the-hood-1pe8</link>
      <guid>https://dev.to/miller_proxy/how-rotating-residential-proxies-handle-ip-assignment-and-session-management-under-the-hood-1pe8</guid>
      <description>&lt;p&gt;You've been using rotating residential proxies long enough to know the basics: one entry point, a pool of IPs, requests get rotated. Then you build a multi-threaded scraper expecting 50 different IPs across 50 concurrent workers, and your logs show the same IP cycling through repeatedly. Or you configure a sticky session for a multi-step checkout flow and it breaks mid-way with no meaningful error. Both of those problems trace back to the same root cause — you're treating the proxy system as a black box when the actual mechanics are knowable and specific.&lt;/p&gt;

&lt;p&gt;This article maps out exactly how backconnect gateways route requests, what triggers rotation at the protocol level, and how sticky sessions are implemented under the hood. The goal is that you leave here able to configure with precision, not trial-and-error.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does a Backconnect Gateway Actually Route Your Requests?
&lt;/h2&gt;

&lt;p&gt;The gateway is the architectural linchpin of the entire system. Your script connects to a single fixed endpoint — one hostname, one port — and that's the only address it ever "sees." Every IP assignment, routing decision, and rotation event happens behind that endpoint, invisible to the client.&lt;/p&gt;

&lt;p&gt;When a request arrives at the gateway, the routing layer selects an exit node from the pool and proxies traffic through it before a single byte reaches the target site. The full flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Script
    ↓
Gateway (single endpoint, e.g. gate.provider.com:7000)
    ↓
IP Pool Routing Layer
    ↓
Exit Node (Residential Device)
    ↓
Target Site
    ↓ (response travels back the same path)
Your Script
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The target site sees only the exit node's IP. Your real IP never appears in the transaction. And critically, the gateway owns the full transaction — it authenticates with the exit node, manages the upstream TCP tunnel, and ensures that neither the exit node nor the target site ever resolves your client's identity.&lt;/p&gt;

&lt;p&gt;This is why all rotation behavior is provider-side. You're not cycling through a list of IPs in your own code — you're making a new TCP connection to the gateway and relying on its routing layer to pick a different exit node. That architectural distinction becomes very important when debugging why rotation isn't working.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Do Real Home IPs Enter the Pool?
&lt;/h2&gt;

&lt;p&gt;Residential IPs come from actual consumer devices — home routers, laptops, mobile phones — whose owners have opted into a bandwidth-sharing arrangement. The typical mechanism is an SDK embedded in a free app or browser extension: the device owner installs it, agrees to the terms, and their idle bandwidth gets contributed to the proxy network.&lt;/p&gt;

&lt;p&gt;The technical implication is that residential exit nodes are not servers. They don't have datacenter uptime SLAs. A device that was online during a health check might go offline two minutes later because the user closed their laptop or their ISP rebooted the router. That authenticity advantage is genuine, but it's paired with an availability trade-off you don't get with datacenter proxies. Exit node dropout — devices going offline mid-session — is the single most common cause of unexpected sticky session failures, and handling it gracefully is covered in the troubleshooting section.&lt;/p&gt;

&lt;p&gt;It also explains why residential IPs carry higher trust scores with most target sites. The IP is assigned to a real ISP customer with a legitimate usage history. Traffic through it looks behaviorally like a real home user, not a server rack.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does the Gateway Pick an IP for Your Request?
&lt;/h2&gt;

&lt;p&gt;The gateway's IP selection isn't random — it's a multi-factor routing decision that happens in milliseconds on every new connection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Geographic filtering&lt;/strong&gt; is applied first. When you specify targeting parameters (&lt;code&gt;country&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;isp&lt;/code&gt;), the routing layer filters the active pool down to exit nodes whose geo attributes match. Reputable providers cross-check node location against multiple geolocation databases — MaxMind, IP2Location, and IPinfo — because geographic inconsistency between databases is itself a detection signal on the target side. Nodes that don't meet the geographic match threshold for your specified targeting don't enter the candidate set at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node health status&lt;/strong&gt; further narrows the pool. The gateway continuously probes exit nodes to verify they're online, responsive, and not on blocklists. IP pool operators typically run these health probes on rolling intervals — anywhere from every 15 minutes for high-priority pools to every 60 minutes across larger pool segments — automatically quarantining nodes where connection success rates fall below the provider's internal thresholds. A blocked or slow-responding exit node gets pulled from the active rotation queue and placed in a cooldown state. The gateway retests it periodically; if it recovers, it gets re-admitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load distribution&lt;/strong&gt; prevents over-concentration on popular exit nodes. The routing layer tracks active connection counts per exit node and avoids funneling too many requests through the same node — both because it degrades that node's performance and because unusually high request volume from a single residential IP is itself a detection pattern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your rotation strategy&lt;/strong&gt; feeds into the final routing decision. Per-request, time-based, and sticky session modes all modify what the routing layer does with the candidate set. This is where your configuration choices actually land.&lt;/p&gt;




&lt;h2&gt;
  
  
  When and How Does the Proxy Switch IPs?
&lt;/h2&gt;

&lt;p&gt;Three distinct rotation models, each triggering at a different point in the request lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-request rotation&lt;/strong&gt; swaps the exit node on every new TCP connection to the gateway — and that phrase "new TCP connection" is doing a lot of work. HTTP/1.1 keep-alive and connection pooling in most HTTP clients maintain a single persistent TCP connection to the proxy endpoint and multiplex multiple HTTP requests over it. If your Python &lt;code&gt;requests.Session()&lt;/code&gt; object holds a keep-alive connection to the gateway, every subsequent request in that session rides through the same TCP tunnel — and therefore hits the same exit node, even with per-request rotation configured.&lt;/p&gt;

&lt;p&gt;This is the most frequently reported rotation failure in production. Symptom: per-request rotation configured, same IP appearing repeatedly in logs. Cause: the HTTP client is reusing the TCP connection to the backconnect gateway. Per-request rotation can only trigger on a &lt;em&gt;new&lt;/em&gt; connection. The fix is to ensure your code isn't inadvertently keeping connections alive — more on the specific mechanics in the troubleshooting section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-based rotation&lt;/strong&gt; runs entirely server-side. The gateway maintains a timer per client; when the configured interval expires, the next request from that client is routed through a new exit node regardless of the connection state. From your code's perspective, nothing changes — same endpoint, same credentials, same port. The IP changes when the gateway's timer fires. This mode is effectively "set and forget" — no connection management required on your end.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Forced rotation&lt;/strong&gt; gives you client-side control over exactly when a switch happens. By changing the session parameter in the proxy username between requests, you signal to the gateway to treat this as a new session and assign a fresh exit node. This is different from sticky sessions (which hold an IP &lt;em&gt;constant&lt;/em&gt;) — here you're using the same session parameter mechanism to actively &lt;em&gt;force&lt;/em&gt; a change. The format is identical to what sticky sessions use, just with a different token per request rather than a constant one — that format is covered in detail in the next section.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrent requests&lt;/strong&gt; deserve a dedicated note. Each independent TCP connection to the gateway is its own routing event — the gateway assigns a different exit node per connection. Twenty concurrent connections should yield up to twenty distinct IPs. But if your HTTP client or thread pool multiplexes those 20 requests over fewer TCP connections (common with HTTP/2 or shared connection pools), you'll get fewer unique IPs than you expect. For maximum IP diversity in concurrent scraping, each worker needs its own isolated TCP connection to the gateway — not a shared pool.&lt;/p&gt;

&lt;p&gt;Those mechanics explain &lt;em&gt;when&lt;/em&gt; IP changes happen. The more interesting question for stateful workflows is how to &lt;em&gt;prevent&lt;/em&gt; them — which is where session management comes in.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Does Sticky Session Actually Work at the Protocol Level?
&lt;/h2&gt;

&lt;p&gt;Sticky session works through a routing table the gateway maintains, keyed on a token you embed in the proxy authentication header.&lt;/p&gt;

&lt;p&gt;Standard proxy authentication uses &lt;code&gt;username:password&lt;/code&gt;. For sticky sessions, you extend the username field with a session identifier. The general format looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;username-session-{your_token}:password@gateway.host:port
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Providers implement this with their own field names. &lt;a href="https://help.decodo.com/docs/residential-proxy-custom-sticky-sessions" rel="noopener noreferrer"&gt;Decodo's residential proxy documentation&lt;/a&gt;, for example, uses &lt;code&gt;user-username-session-example1-sessionduration-90:password&lt;/code&gt; where &lt;code&gt;sessionduration&lt;/code&gt; sets how many minutes the session should persist. SOAX uses &lt;code&gt;package-{id}-country-us-sessionid-rand3-sessionlength-600&lt;/code&gt;. The mechanism is the same across providers: the session token is embedded in the username string, parsed by the gateway's authentication layer, and used as a lookup key in the routing table.&lt;/p&gt;

&lt;p&gt;When the gateway receives a request with a session token it recognizes, it skips the pool selection step entirely and routes directly to the exit node mapped to that token. When it sees a new or expired token, it runs the full selection process and creates a new mapping.&lt;/p&gt;

&lt;p&gt;Session tokens have a TTL. Decodo's default is 10 minutes for residential proxies, with sessions configurable up to 24 hours depending on the endpoint type. When TTL expires, the mapping entry is dropped, and the next request with that token is treated as a new session. There's a second termination trigger that's less intuitive: if the exit node goes offline mid-session (the residential device disconnects), the gateway can't fulfill the routing promise. Some providers silently migrate you to a new exit node; others return a 503 error. If your sticky session breaks before the TTL expires with no clear error, the exit node going offline is the most likely explanation.&lt;/p&gt;

&lt;p&gt;Running multiple concurrent sticky sessions works cleanly — different tokens map to different exit nodes in the routing table. You can maintain 50 parallel sticky sessions with 50 distinct tokens and get 50 persistent IPs simultaneously. The practical limit is your subscription's concurrent connection allowance.&lt;/p&gt;

&lt;h3&gt;
  
  
  Verifying Your Session Configuration
&lt;/h3&gt;

&lt;p&gt;Before wiring any of this into production code, it's worth confirming your proxy handles both modes as expected. You need Python 3 with &lt;code&gt;requests&lt;/code&gt; installed and valid credentials from your proxy provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="c1"&gt;# Your gateway endpoint and credentials — find these in your provider's dashboard
&lt;/span&gt;&lt;span class="n"&gt;GATEWAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your.proxy-gateway.com:7000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;USER&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PASS&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;your_password&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;CHECK&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://httpbin.org/ip&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_proxies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;USER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-session-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;USER&lt;/span&gt;
    &lt;span class="n"&gt;proxy_url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PASS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;@&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;GATEWAY&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;proxy_url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Test 1: Per-request rotation — each call should return a different IP.
# Note: using requests.get() directly, NOT a Session object.
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=== Per-request rotation ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHECK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;make_proxies&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Request &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;origin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Test 2: Sticky session — all three calls should return the same IP.
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;=== Sticky session (token: test-sticky-01) ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHECK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;make_proxies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-sticky-01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Request &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;origin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Test 3: New session token — should return a different IP than Test 2.
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;=== New session token (test-sticky-02) ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHECK&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;make_proxies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test-sticky-02&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  Request 1: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;origin&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Proxy001, the exact gateway endpoint and credential format are in your dashboard's connection guide — the credential generator outputs the correct &lt;code&gt;session&lt;/code&gt; field name and separators for your account type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt; Test 1 should print three different IPs. Test 2 should print the same IP three times. Test 3 should print a different IP than any from Test 2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If Test 1 shows the same IP repeating:&lt;/strong&gt; Connection pooling is the culprit. Using &lt;code&gt;requests.get()&lt;/code&gt; directly (as above) creates a fresh connection each time. If you're using a &lt;code&gt;Session()&lt;/code&gt; object, add &lt;code&gt;"Connection": "close"&lt;/code&gt; to your headers, or close and recreate the session per request. The &lt;a href="https://urllib3.readthedocs.io/en/stable/advanced-usage.html" rel="noopener noreferrer"&gt;urllib3 advanced usage documentation&lt;/a&gt; covers connection pool configuration in detail if you need finer-grained control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If Test 2 shows different IPs:&lt;/strong&gt; The session token isn't being parsed correctly. Check the exact field separator and parameter names in your provider's documentation — &lt;code&gt;-session-&lt;/code&gt; is common, but providers are inconsistent on this. The credential generator in your provider's dashboard outputs the correct format.&lt;/p&gt;

&lt;p&gt;A quick curl equivalent for Test 2:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Run this twice — both should return the same IP&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-U&lt;/span&gt; &lt;span class="s2"&gt;"username-session-test-sticky-01:password"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="s2"&gt;"your.proxy-gateway.com:7000"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     https://httpbin.org/ip | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're getting connection timeouts on Test 2, the assigned exit node may be temporarily offline. Retry with a different session token — the gateway will allocate a fresh node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliant use:&lt;/strong&gt; Residential proxies are a legitimate tool when the target is a publicly accessible resource and your activity doesn't violate the site's Terms of Service or applicable law. Don't route proxy traffic through authenticated systems you're not authorized to access. If your work involves collecting data that includes personal information — user reviews, public profiles, pricing tied to individual accounts — GDPR, CCPA, and equivalent laws govern what you do with that data, independent of how the proxy is configured. In commercial scraping operations, maintaining session logs and audit trails is standard practice.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Is My IP Rotating Unexpectedly (or Not at All)?
&lt;/h2&gt;

&lt;p&gt;Four failure patterns cover the majority of rotation and session issues in practice. They break into two categories: connection-level problems, and target-side detection problems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection-level failures&lt;/strong&gt; are the more fixable of the two. If your sticky session drops before the TTL expires, the exit node went offline. Residential devices disconnect — users close laptops, ISPs recycle connections, the bandwidth-sharing app gets killed in the background. You can't prevent this, but you can handle it: catch &lt;code&gt;requests.exceptions.ProxyError&lt;/code&gt; or a 503 response code, generate a new session token, and retry from the beginning of your multi-step flow. Logging the response code (or the error type on connection failure) helps distinguish node dropout from token expiry — the former tends to arrive as a connection reset, the latter as a clean expiry followed by a new IP assignment on retry.&lt;/p&gt;

&lt;p&gt;If per-request rotation isn't rotating, the cause is almost always connection reuse. Python's &lt;code&gt;requests.Session()&lt;/code&gt; object enables keep-alive via urllib3 by default, so all requests made through the session reuse the same TCP connection to the gateway. Use standalone &lt;code&gt;requests.get()&lt;/code&gt; calls, or if you need a session for cookie handling, add &lt;code&gt;session.headers.update({"Connection": "close"})&lt;/code&gt; and call &lt;code&gt;session.close()&lt;/code&gt; after each target domain transaction. In concurrent setups, verify your thread pool isn't sharing a connection pool: a &lt;code&gt;Session&lt;/code&gt; object shared across threads maintains a shared urllib3 pool, so multiple workers may route through fewer actual TCP connections than you have threads. Give each worker its own independent connection management.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Target-side blocks that persist across IP changes&lt;/strong&gt; are a different class of problem. When you're getting blocked even after rotating to a confirmed-fresh IP, the block isn't on the IP address. Target sites increasingly fingerprint on TLS client hello characteristics, HTTP/2 pseudo-header ordering, cookie residue from previous sessions, and request timing patterns. IP rotation won't fix any of these. If you've confirmed the new IP is clean by testing against &lt;code&gt;httpbin.org/ip&lt;/code&gt; and a fresh request to the target in isolation, the issue is in your client configuration, not the proxy layer.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to Configure These Mechanisms in Practice
&lt;/h2&gt;

&lt;p&gt;A direct mapping from use case to the right mode and the parameters that matter:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Rotation Mode&lt;/th&gt;
&lt;th&gt;Configuration Priority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Stateless product / price scraping&lt;/td&gt;
&lt;td&gt;Per-request&lt;/td&gt;
&lt;td&gt;Disable keep-alive; fresh TCP connection per request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Multi-step login or checkout flow&lt;/td&gt;
&lt;td&gt;Sticky session&lt;/td&gt;
&lt;td&gt;TTL ≥ max expected session duration; handle ProxyError for node dropout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;High-volume concurrent SERP crawling&lt;/td&gt;
&lt;td&gt;Per-request, per-worker isolation&lt;/td&gt;
&lt;td&gt;One connection per worker; no shared connection pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Legitimate multi-account workflows (e.g., managing ad accounts or storefronts you own and are authorized to operate)&lt;/td&gt;
&lt;td&gt;Multiple sticky sessions&lt;/td&gt;
&lt;td&gt;Unique token per account; ensure all accounts are under your organization's authorization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geo-targeted verification&lt;/td&gt;
&lt;td&gt;Either mode + geo params&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;country&lt;/code&gt;, &lt;code&gt;city&lt;/code&gt;, &lt;code&gt;isp&lt;/code&gt; in username or API params&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Geographic targeting and session parameters are both configured through the proxy username field in most backconnect providers — no separate API call needed for standard configurations. The exact field names (&lt;code&gt;-country-&lt;/code&gt;, &lt;code&gt;-city-&lt;/code&gt;, &lt;code&gt;-session-&lt;/code&gt;, &lt;code&gt;-sessionlength-&lt;/code&gt;) are provider-specific. Most backconnect providers expose both parameters through their dashboard credential generator — consult your provider's setup documentation for the exact field names and separators for your account type.&lt;/p&gt;




&lt;h2&gt;
  
  
  What to Do With This Now
&lt;/h2&gt;

&lt;p&gt;Three concrete steps from here.&lt;/p&gt;

&lt;p&gt;Run the verification script from the session section against your current proxy configuration. Confirm that per-request rotation and sticky session behavior match your expectations &lt;em&gt;before&lt;/em&gt; wiring either into production code. The test takes under five minutes and saves hours of debugging later.&lt;/p&gt;

&lt;p&gt;Audit concurrent setups for connection pooling. If your multi-worker scraper isn't achieving the IP diversity you expect, check whether your HTTP client is multiplexing worker threads onto shared connections. The fix is typically a one-line change — the problem is that it's invisible in logs unless you're actively printing the source IP per request.&lt;/p&gt;

&lt;p&gt;Map your use cases to the rotation mode table. Sticky session and per-request rotation are both correct choices — for different jobs. Running per-request rotation on a stateful authenticated flow will break session continuity. Running sticky session on a high-volume stateless crawl wastes IP pool diversity. Neither failure is the proxy's fault; it's a configuration mismatch.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you want to run these tests against a production-grade residential IP pool, &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; offers a free trial with full access to sticky session and per-request rotation on the same endpoint. The pool spans 100M+ IPs across 200+ regions, with real ISP-assigned residential addresses — you get a live environment to verify everything covered in this article before committing to a plan. The dashboard credential generator outputs the exact username format for your session and geo parameters, so there's no manual string assembly. &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Request your free trial&lt;/a&gt; and run the verification script in this article within minutes.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>backend</category>
      <category>networking</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>WebRTC Leaks: Why Even Premium Residential Proxies Are Getting Detected in 2026</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Tue, 10 Mar 2026 02:18:41 +0000</pubDate>
      <link>https://dev.to/miller_proxy/webrtc-leaks-why-even-premium-residential-proxies-are-getting-detected-in-2026-4o9c</link>
      <guid>https://dev.to/miller_proxy/webrtc-leaks-why-even-premium-residential-proxies-are-getting-detected-in-2026-4o9c</guid>
      <description>&lt;p&gt;You just paid for a premium residential proxy service. Clean IPs, real ISP registration, the works. You verify the IP—looks good. Then five minutes into your session, the site drops a CAPTCHA or quietly flags your account as a suspicious proxy user.&lt;/p&gt;

&lt;p&gt;The proxy didn't fail. Something else did.&lt;/p&gt;

&lt;p&gt;In most of these cases, the culprit is WebRTC—a browser communication protocol that ignores your proxy entirely and broadcasts your real IP to any site that asks. This isn't a bug in your proxy service, and a better proxy won't fix it. It's an architectural mismatch between how proxies work and how WebRTC works, operating at completely different layers of your browser stack.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Your Residential Proxy Can't Block a WebRTC Leak
&lt;/h2&gt;

&lt;p&gt;The root cause is transport protocol. HTTP and SOCKS5 proxies intercept TCP traffic—that's what they're built to route. WebRTC uses UDP for its peer discovery process.&lt;/p&gt;

&lt;p&gt;When a browser initiates a WebRTC connection, it runs an ICE (Interactive Connectivity Establishment) candidate gathering process. The browser sends STUN requests to external servers over UDP on port 3478. Per &lt;a href="https://www.rfc-editor.org/rfc/rfc5389.html" rel="noopener noreferrer"&gt;RFC 5389&lt;/a&gt;, this mechanism is explicitly designed to traverse NATs and firewalls—meaning it intentionally routes around intermediary systems that only handle TCP. &lt;a href="https://www.rfc-editor.org/rfc/rfc8828.html" rel="noopener noreferrer"&gt;RFC 8828&lt;/a&gt; specifically addresses this gap, noting that WebRTC traffic &lt;em&gt;should&lt;/em&gt; follow the same proxy path as HTTP traffic—but that requires active browser configuration. It doesn't happen by default. &lt;a href="https://datatracker.ietf.org/doc/draft-ietf-rtcweb-ip-handling/09/" rel="noopener noreferrer"&gt;datatracker.ietf&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As &lt;a href="https://stackoverflow.com/questions/54900765/how-is-stun-able-to-bypass-a-proxy" rel="noopener noreferrer"&gt;a Stack Overflow analysis of STUN proxy bypass&lt;/a&gt; explains, the STUN client "literally just creates a socket and sends as it would if there were no proxy settings"—your proxy handles your HTTP traffic flawlessly while the STUN request exits through your real network interface, carrying your real IP with it. &lt;a href="https://stackoverflow.com/questions/54900765/how-is-stun-able-to-bypass-a-proxy" rel="noopener noreferrer"&gt;stackoverflow&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four Types of WebRTC Leaks (Two Will End Your Session)
&lt;/h2&gt;

&lt;p&gt;Not all WebRTC leaks carry the same risk. Knowing which type you're dealing with determines what you need to fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public IP leak&lt;/strong&gt; — Your real external IP appears in the WebRTC ICE candidates alongside or instead of the proxy IP. This directly flags you to any site running a detection check. Immediately fatal for proxy sessions. &lt;a href="https://www.proxies.sx/use-cases/privacy/webrtc-leak" rel="noopener noreferrer"&gt;proxies&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPv6 leak&lt;/strong&gt; — The most underdiagnosed problem for proxy users. Most residential proxy services route IPv4 traffic only, but if your machine has an active IPv6 connection, that address will appear in WebRTC ICE candidates. IPv6 addresses are device-specific and far harder to rotate. Most standard leak tests only check IPv4—so you can pass a basic test and still be leaking. If you've "fixed" your WebRTC situation but are still getting flagged, always run a dedicated IPv6 check at ipleak.net's WebRTC section. &lt;a href="https://www.proxies.sx/use-cases/privacy/webrtc-leak" rel="noopener noreferrer"&gt;proxies&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;STUN request leak&lt;/strong&gt; — The STUN server you contact during ICE gathering sees your real IP, even if it isn't passed back to the calling site. Relevant for workflows where third-party logging is a concern.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local IP leak&lt;/strong&gt; — Your internal LAN address (192.168.x.x or 10.x.x.x) appears in ICE candidates. This alone doesn't expose your real external IP, but a consistent local address across multiple sessions is a cross-session linkability signal.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Triggers False Proxy Detection on Your Residential Sessions?
&lt;/h2&gt;

&lt;p&gt;Two configuration gaps cause the majority of false positive flags on correctly intended proxy operations. &lt;a href="https://scrapfly.io/web-scraping-tools/webrtc-leak" rel="noopener noreferrer"&gt;scrapfly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP inconsistency&lt;/strong&gt;: The IP gathered via WebRTC doesn't match the IP at the HTTP connection layer. If your proxy routes through a German residential IP but WebRTC returns your real home connection, that mismatch is the most direct trigger—a clear sign your browser configuration is incomplete, not that your proxy is low quality.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structural anomaly&lt;/strong&gt;: A browser with WebRTC fully disabled returns zero ICE candidates. Zero candidates is statistically abnormal and correlates strongly with privacy tool misconfiguration. This is its own detection trigger, separate from and sometimes worse than an IP mismatch—which is why "just disable WebRTC" is the wrong fix.&lt;/p&gt;

&lt;p&gt;If both gaps are closed but you're still getting flagged, other fingerprinting signals (canvas, TLS, fonts, request timing) are compounding the problem. That's what the antidetect browser approach in Case B addresses.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "Disable WebRTC" Is the Wrong Fix
&lt;/h2&gt;

&lt;p&gt;Fully disabling WebRTC—the advice in most generic privacy guides—creates a different configuration problem for proxy users.&lt;/p&gt;

&lt;p&gt;When WebRTC is fully disabled (&lt;code&gt;media.peerconnection.enabled = false&lt;/code&gt; in Firefox, or the equivalent elsewhere), your browser returns zero ICE candidates when any detection script queries it. As documented in hands-on testing shared in the &lt;a href="https://www.reddit.com/r/AntiDetectGuides/comments/1rg01n2/stop_leaking_your_webrtc_a_deep_dive_into_why/" rel="noopener noreferrer"&gt;r/AntiDetectGuides community&lt;/a&gt;, profiles with WebRTC fully disabled were flagged at significantly higher rates than profiles using Replace mode—the disabled state is itself a recognizable signal. Beyond detection, it also violates RFC 8828's guidance that WebRTC traffic should follow the proxy path rather than be suppressed entirely, and it breaks any legitimate site features that depend on WebRTC. &lt;a href="https://www.reddit.com/r/AntiDetectGuides/comments/1rg01n2/stop_leaking_your_webrtc_a_deep_dive_into_why/" rel="noopener noreferrer"&gt;reddit&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The correct configuration is &lt;strong&gt;Replace mode&lt;/strong&gt;: WebRTC runs normally, but ICE candidate gathering is constrained to network interfaces that route through your proxy. Your browser produces a normal-looking set of candidates; the external IP those candidates report is your proxy IP, not your real IP. In Chrome-based browsers, "Disable non-proxied UDP" achieves exactly this. WebRTC is fully operational—it just can't reach any interface outside the proxy path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Diagnose Your Setup Right Now
&lt;/h2&gt;

&lt;p&gt;Before touching any configuration, confirm whether you actually have a WebRTC leak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: Your proxy must already be active and routing HTTP traffic—verify this first with any standard IP lookup tool. Run this test in the exact browser or automation environment you use for your actual sessions, not a different browser on the same machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1&lt;/strong&gt; — With the proxy active, navigate to &lt;code&gt;browserleaks.com/webrtc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2&lt;/strong&gt; — Locate the "Public IP Address" row in the ICE candidate table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3&lt;/strong&gt; — Match what you see:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Next step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Public IP = proxy IP only&lt;/td&gt;
&lt;td&gt;✅ No leak&lt;/td&gt;
&lt;td&gt;Run IPv6 check at ipleak.net to confirm full coverage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Public IP = your real ISP IP&lt;/td&gt;
&lt;td&gt;❌ Full leak&lt;/td&gt;
&lt;td&gt;Apply the relevant fix in the next section immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Both proxy IP and real IP present&lt;/td&gt;
&lt;td&gt;⚠️ Partial leak&lt;/td&gt;
&lt;td&gt;Still a leak — apply the same fix as above&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero candidates / "No candidates"&lt;/td&gt;
&lt;td&gt;⚠️ WebRTC disabled&lt;/td&gt;
&lt;td&gt;Read the previous section — this state is also a flag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here's what a partial leak actually looks like: you'll see two separate entries under "Public IP Address" — one matching your proxy IP (e.g., a German Telekom address), and a second entry showing your real ISP IP. Both are visible simultaneously.&lt;/p&gt;

&lt;p&gt;One caveat: browserleaks focuses primarily on IPv4. If your residential proxy is IPv4-only but your machine has an active IPv6 address, the IPv4 check may show clean while IPv6 leaks through a separate ICE candidate. Always follow up with ipleak.net's WebRTC section.&lt;/p&gt;

&lt;p&gt;For automated stacks, add a programmatic WebRTC check to your session startup sequence and treat a failing result as a hard launch blocker.&lt;/p&gt;




&lt;h2&gt;
  
  
  Fix It: Solutions by Your Setup
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Case A — Manual Browser Users (Chrome, Brave, Firefox)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Chrome&lt;/strong&gt; doesn't expose WebRTC handling in its settings UI. Install the &lt;strong&gt;WebRTC Network Limiter&lt;/strong&gt; extension published by Google, open its options, and set "IP handling policy" to &lt;strong&gt;"Disable non-proxied UDP (force proxy)"&lt;/strong&gt;. WebRTC stays active but can only operate over interfaces routed through your configured proxy. &lt;a href="https://actproxy.com/knowledgebase/31/How-to-protect-yourself-from-showing-your-real-IP-with-WebRTC.html" rel="noopener noreferrer"&gt;actproxy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brave&lt;/strong&gt; exposes this natively: Settings → Additional Settings → Privacy and Security → WebRTC IP Handling Policy → &lt;strong&gt;"Disable non-proxied UDP"&lt;/strong&gt;. &lt;a href="https://community.brave.app/t/webrtc-ip-handling-policy-stuck-on-disable-non-proxied-udp/66722" rel="noopener noreferrer"&gt;community.brave&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firefox&lt;/strong&gt;: Don't touch &lt;code&gt;media.peerconnection.enabled&lt;/code&gt;. Instead, open &lt;code&gt;about:config&lt;/code&gt; and set: &lt;a href="https://roundproxies.com/blog/webrtc-leaks/" rel="noopener noreferrer"&gt;roundproxies&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;media.peerconnection.ice.default_address_only&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;media.peerconnection.ice.no_host&lt;/span&gt;                    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;media.peerconnection.ice.proxy_only_if_behind_proxy&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;WebRTC continues to function normally; it just can't access your real IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Critical&lt;/strong&gt;: &lt;code&gt;proxy_only_if_behind_proxy&lt;/code&gt; requires a proxy configured in Firefox's &lt;em&gt;network panel&lt;/em&gt; — Settings → General → Network Settings → Manual proxy configuration. If that panel still shows "No proxy" or "Use system proxy settings," the about:config entries are correct but will silently do nothing. This is the single most common reason the Firefox fix appears applied but doesn't work. Tested on Firefox 123 on macOS and Windows, March 2026.&lt;/p&gt;




&lt;h3&gt;
  
  
  Case B — Antidetect Browser Users
&lt;/h3&gt;

&lt;p&gt;Antidetect browsers handle WebRTC at the browser engine API level rather than network policy. The target behavior is the same across tools: WebRTC runs normally, and ICE candidates report your proxy IP rather than your real IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multilogin&lt;/strong&gt;: In profile creation or editing, navigate to the &lt;strong&gt;Fingerprint&lt;/strong&gt; tab → &lt;strong&gt;WebRTC&lt;/strong&gt; section. Select &lt;strong&gt;"Masked"&lt;/strong&gt; — this is Multilogin's label for Replace mode, where WebRTC reports your proxy IP rather than your real IP. Do not select "Disabled." &lt;a href="https://multilogin.com/help/en_US/profile-settings-fingerprint-section" rel="noopener noreferrer"&gt;multilogin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GoLogin&lt;/strong&gt;: As of v4.0.11 (February 19, 2026), the WebRTC option moved to &lt;strong&gt;Profile Settings → Advanced → WebRTC&lt;/strong&gt;. Select &lt;strong&gt;"Altered"&lt;/strong&gt; — GoLogin's equivalent of Replace mode, which sets both Public IP and Local IP in WebRTC to match your assigned proxy IP. Do not select "Disabled." &lt;a href="https://support.gologin.com/en/articles/3453362-webrtc-profile-settings" rel="noopener noreferrer"&gt;support.gologin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Other tools&lt;/strong&gt; (AdsPower, Dolphin Anty, Octo Browser, etc.): Look for the WebRTC setting in the fingerprint or privacy section. Any mode labeled "Replace," "Substitute," "Proxy," or similar maps to the same Replace mode behavior. Avoid anything labeled "Disable" or "Block."&lt;/p&gt;

&lt;p&gt;One detail that catches a lot of users: antidetect browsers handle the fingerprint layer, but the IP quality those candidates report depends entirely on your underlying proxy. For use cases requiring precise geographic consistency—ad verification, localization QA, or authorized multi-account management for businesses operating multiple verified brand accounts on platforms that explicitly permit it—you need live residential proxies that are both clean and accurately geolocated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; provides access to 100M+ real residential and ISP proxies across 200+ countries, with targeting down to city and carrier level. That granularity matters specifically for antidetect browser setups where the proxy IP's reported ISP and location should match the browser profile's declared identity.&lt;/p&gt;




&lt;h3&gt;
  
  
  Case C — Automation Stack (Playwright, Puppeteer, Selenium)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites&lt;/strong&gt;: Node.js 18+ for Playwright/Puppeteer; Python 3.8+ and selenium &amp;gt;= 4.0 for Selenium. Proxy host, port, username, and password from your provider's dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Playwright (Node.js)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://proxy.your-provider.com:PORT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_USERNAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--force-webrtc-ip-handling-policy=disable_non_proxied_udp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="c1"&gt;// Verify before starting your actual task&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://browserleaks.com/webrtc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Confirm reported public IP matches your expected proxy IP&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are &lt;a href="https://github.com/microsoft/playwright/issues/13516" rel="noopener noreferrer"&gt;documented reliability issues&lt;/a&gt; with &lt;code&gt;--force-webrtc-ip-handling-policy&lt;/code&gt; propagating correctly in some Chromium builds.  For production use, add an explicit WebRTC assertion at session startup and treat a mismatch as a hard failure. Verified against Playwright 1.42 / Chromium 123, March 2026. &lt;a href="https://github.com/microsoft/playwright/issues/13516" rel="noopener noreferrer"&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Puppeteer (Node.js)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Puppeteer doesn't support a native &lt;code&gt;proxy&lt;/code&gt; option with authentication at the launch level — handle proxy auth via &lt;code&gt;page.authenticate()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--proxy-server=http://proxy.your-provider.com:PORT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--force-webrtc-ip-handling-policy=disable_non_proxied_udp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_USERNAME&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;YOUR_PASSWORD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://browserleaks.com/webrtc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Verify proxy IP appears in WebRTC candidates before continuing&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;})();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For high-volume rotation using backconnect residential proxies, &lt;code&gt;page.authenticate()&lt;/code&gt; persists for the lifetime of the page object—authenticate once per page, not per browser instance. Verified against Puppeteer 22 / Chromium 123, March 2026.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Selenium (Python)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The preference-based approach is more consistent than CLI flags: &lt;a href="https://stackoverflow.com/questions/62582674/how-to-set-the-value-of-chrome-privacy-network-webrtciphandlingpolicy-using-sele" rel="noopener noreferrer"&gt;stackoverflow&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;

&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--proxy-server=http://proxy.your-provider.com:PORT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_experimental_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prefs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webrtc.ip_handling_policy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;disable_non_proxied_udp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webrtc.multiple_routes_enabled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webrtc.nonproxied_udp_enabled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Verified against Chrome 122 / selenium 4.18, February 2026. Authenticated proxies require an extension-based approach in Selenium — see the selenium-wire library or a Chrome extension injected at launch for username/password authentication.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Pre-Task Verification Checklist
&lt;/h2&gt;

&lt;p&gt;Run this before any session. Browser updates and proxy configuration changes can silently reintroduce leaks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 1 — IP layer&lt;/strong&gt; (&lt;a href="https://browserleaks.com/webrtc" rel="noopener noreferrer"&gt;browserleaks.com/webrtc&lt;/a&gt;)&lt;br&gt;
Public IP in ICE candidates should match your proxy IP only. Candidate count should be greater than zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 2 — IPv6 coverage&lt;/strong&gt; (ipleak.net → WebRTC section)&lt;br&gt;
If your real IPv6 address appears here, either confirm your proxy provider routes IPv6 (check their documentation), or disable IPv6 at the OS level:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Windows&lt;/strong&gt; (elevated Command Prompt): &lt;code&gt;netsh interface ipv6 set global disabled&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS&lt;/strong&gt;: System Settings → Network → your active interface → Details → TCP/IP → Configure IPv6 → &lt;strong&gt;Off&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linux&lt;/strong&gt;: Add &lt;code&gt;net.ipv6.conf.all.disable_ipv6 = 1&lt;/code&gt; to &lt;code&gt;/etc/sysctl.conf&lt;/code&gt;, then run &lt;code&gt;sudo sysctl -p&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This affects all network activity on the machine—re-enable when done if other workflows need IPv6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 3 — WebRTC fingerprint structure&lt;/strong&gt; (&lt;a href="https://scrapfly.io/web-scraping-tools/webrtc-leak" rel="noopener noreferrer"&gt;scrapfly.io/web-scraping-tools/webrtc-leak&lt;/a&gt;)&lt;br&gt;
A typical Chrome 122+ session under a clean residential proxy should show H264, VP8, VP9, and AV1 codecs with standard RTP extensions. If you see zero codecs or H264 is entirely absent, your WebRTC environment has been modified in a detectable way beyond just the IP. The exact codec list varies by OS and GPU, but a completely stripped codec set is the red flag. &lt;a href="https://scrapfly.io/web-scraping-tools/webrtc-leak" rel="noopener noreferrer"&gt;scrapfly&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check 4 — Proxy IP reputation&lt;/strong&gt; (ipqualityscore.com or scamalytics.com)&lt;br&gt;
Confirm the current proxy IP isn't flagged in abuse databases. For rotating residential proxies, run this again if detection behavior appears mid-session—IP rotation may have switched you to a lower-quality address.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Troubleshooting&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;WebRTC checks clean, but CAPTCHAs persist.&lt;/em&gt; Other fingerprinting signals are triggering detection—typically canvas, fonts, TLS fingerprint, or timing anomalies. A patched standard browser addresses WebRTC alone; an antidetect browser addresses the full fingerprint stack. If you're seeing consistent false positives with a verified clean WebRTC setup, move from a modified standard browser to an antidetect browser configured with a matching proxy.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Firefox settings applied, real IP still visible.&lt;/em&gt; Open Firefox Settings → General → Network Settings and confirm your proxy is configured there directly. If the panel shows "No proxy" or "Use system proxy settings," the about:config entries are correct but inactive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Playwright flag works locally, WebRTC leaks in CI.&lt;/em&gt; &lt;code&gt;--force-webrtc-ip-handling-policy&lt;/code&gt; behavior varies across Chromium builds. Add a WebRTC assertion to your startup sequence so regressions fail explicitly.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Compliance note: The configurations described here route WebRTC traffic through a configured proxy, consistent with &lt;a href="https://www.rfc-editor.org/rfc/rfc8828.html" rel="noopener noreferrer"&gt;RFC 8828&lt;/a&gt;'s guidance that WebRTC traffic should follow the same proxy path as HTTP traffic when a proxy is in use. These are standard network configuration practices. Proxy usage should comply with the terms of service of your proxy provider and the sites you access, and should be limited to lawful purposes including data collection, ad verification, security testing, performance benchmarking, and similar legitimate business activities.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Get Clean Residential IPs That Hold Up
&lt;/h2&gt;

&lt;p&gt;Fixing the WebRTC layer removes the most direct detection trigger, but your proxy IP quality still determines how far you get. A correctly configured browser environment paired with a flagged or overused IP pool won't hold up under scrutiny.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; gives you access to 100M+ verified residential and ISP proxies across 200+ countries, with targeting down to city and mobile carrier level—real IPs from real ISPs, not datacenter ranges attempting to pass as residential.&lt;/p&gt;

&lt;p&gt;For automation teams, Proxy001 integrates directly with Python, Node.js, Playwright, Puppeteer, Selenium, and Scrapy, with documentation covering proxy authentication setup in each. For antidetect browser users, carrier-level geo-targeting means you can match the proxy IP's ISP and location to your profile's declared identity—a consistency detail that matters across sessions.&lt;/p&gt;

&lt;p&gt;Try it with a free trial at &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;proxy001.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>networking</category>
      <category>privacy</category>
      <category>security</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Why Do Rotating Residential Proxies Get Blocked After 2-3 Requests</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Mon, 02 Mar 2026 02:32:26 +0000</pubDate>
      <link>https://dev.to/miller_proxy/why-do-rotating-residential-proxies-get-blocked-after-2-3-requests-253b</link>
      <guid>https://dev.to/miller_proxy/why-do-rotating-residential-proxies-get-blocked-after-2-3-requests-253b</guid>
      <description>&lt;p&gt;You paid for premium rotating residential proxies. You configured rotation. You launched your scraper. And within two or three requests, you're staring at a 403, a CAPTCHA wall, or a connection reset.&lt;/p&gt;

&lt;p&gt;We ran into this exact scenario last month while benchmarking proxy providers for an e-commerce price monitoring project. Python &lt;code&gt;requests&lt;/code&gt; through a top-tier residential proxy pool—403 on the second request, every single time. Same IP configured in a regular Chrome browser? Page loaded fine. The proxy wasn't the problem. Our HTTP client's TLS fingerprint was.&lt;/p&gt;

&lt;p&gt;That experience captures the core issue most proxy users miss: modern anti-bot systems don't just check whether your IP is residential. They analyze your TLS handshake, your browser fingerprint, your cookie behavior, and your request cadence. Rotating your IP fixes exactly one of those layers. If the other layers scream "automation," a fresh IP won't save you.&lt;/p&gt;

&lt;p&gt;Below: the six specific causes, a diagnostic table to match your symptoms to the root cause, and targeted fixes for each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern Anti-Bot Detection Goes Far Beyond IP Addresses
&lt;/h2&gt;

&lt;p&gt;Anti-bot systems in 2025-2026 operate as multi-layered detection stacks. Each layer evaluates a different signal independently, and failing any single layer can trigger a block—even if every other layer looks clean.&lt;/p&gt;

&lt;p&gt;Here's what a typical detection stack evaluates, roughly in order of when it fires:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;TLS fingerprint&lt;/strong&gt; — Before your first byte of HTML arrives. The server inspects your TLS ClientHello to determine whether you're a real browser or an HTTP library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP reputation&lt;/strong&gt; — Your IP's history, ASN classification, subnet abuse patterns, and geographic consistency.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP header consistency&lt;/strong&gt; — Whether your headers (User-Agent, Accept-Language, Accept-Encoding, Sec-CH-UA) match what a real browser with that TLS fingerprint would send.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser fingerprint&lt;/strong&gt; — Canvas rendering, WebGL renderer strings, installed fonts, screen resolution, timezone, and dozens of other JavaScript-accessible properties.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Session and cookie behavior&lt;/strong&gt; — Whether cookies persist appropriately within a session and reset appropriately across sessions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Behavioral analysis&lt;/strong&gt; — Request timing, navigation patterns, mouse movement, scroll behavior, and whether you follow a natural browsing path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These layers are &lt;em&gt;independent&lt;/em&gt;. Rotating your IP only addresses layer 2. If layer 1 (TLS) already identified you as a Python script during the handshake, a new residential IP changes nothing—the next request gets flagged just as fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  6 Reasons Your Rotating Proxies Get Blocked Instantly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Your TLS Fingerprint Exposes You Before the First Page Loads
&lt;/h3&gt;

&lt;p&gt;This is the most common reason for getting blocked within 2-3 requests, and it's the one most proxy users never think about.&lt;/p&gt;

&lt;p&gt;Every TLS connection starts with a ClientHello message. This message contains your client's supported cipher suites, TLS extensions, elliptic curves, and their ordering. Anti-bot systems hash these values into a fingerprint—known as a JA3 or JA4 fingerprint—and compare it against a database of known signatures.&lt;/p&gt;

&lt;p&gt;The problem: Python's &lt;code&gt;requests&lt;/code&gt; library, Go's &lt;code&gt;net/http&lt;/code&gt;, and Node.js's &lt;code&gt;axios&lt;/code&gt; all produce TLS fingerprints that look nothing like Chrome, Firefox, or Safari. According to Salesforce's original JA3 research, each client implementation generates a distinct, recognizable signature. When Cloudflare or Akamai sees a residential IP paired with a Python &lt;code&gt;requests&lt;/code&gt; TLS fingerprint, the mismatch is obvious. You're claiming to be a home user browsing with Chrome, but your TLS handshake says you're a script.&lt;/p&gt;

&lt;p&gt;We verified this directly: we pointed the same residential IP at a TLS fingerprint checker from both Python &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;curl_cffi&lt;/code&gt; with Chrome impersonation. The JA3 hashes were completely different—&lt;code&gt;requests&lt;/code&gt; produced a signature that matched no known browser, while &lt;code&gt;curl_cffi&lt;/code&gt; matched Chrome 124 exactly.&lt;/p&gt;

&lt;p&gt;The newer JA4+ standard, developed by FoxIO, makes this even harder to avoid. JA4 normalizes extension ordering (defeating Chrome's TLS extension randomization introduced in 2023) and incorporates TCP and HTTP/2 context, producing a more stable and harder-to-spoof identifier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this causes blocking in 2-3 requests:&lt;/strong&gt; The TLS fingerprint is checked on every single connection. The first request might get through if the site has a permissive initial threshold, but by the second or third request from a "residential IP" with a non-browser TLS signature, the bot score crosses the blocking threshold.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Your Browser Fingerprint Stays the Same Across IPs
&lt;/h3&gt;

&lt;p&gt;You rotate from IP-A (Dallas, TX) to IP-B (São Paulo, BR) to IP-C (London, UK). But all three requests carry the same User-Agent string, the same canvas hash, the same WebGL renderer, the same screen resolution, and the same timezone offset.&lt;/p&gt;

&lt;p&gt;To the anti-bot system, this pattern is unmistakable: three "different users" from three continents sharing an identical device fingerprint. That doesn't happen with real users.&lt;/p&gt;

&lt;p&gt;There's also the consistency problem. If your IP geolocates to London but your browser's &lt;code&gt;Intl.DateTimeFormat().resolvedOptions().timeZone&lt;/code&gt; returns &lt;code&gt;America/New_York&lt;/code&gt; and your &lt;code&gt;Accept-Language&lt;/code&gt; header starts with &lt;code&gt;en-US&lt;/code&gt;, the mismatch between network-level and application-level signals flags the request as proxied.&lt;/p&gt;

&lt;p&gt;Headless browsers add another wrinkle. Unmodified Puppeteer and Playwright instances expose automation markers—&lt;code&gt;navigator.webdriver&lt;/code&gt; set to &lt;code&gt;true&lt;/code&gt;, missing plugin arrays, and Chrome DevTools Protocol artifacts—that anti-bot systems specifically scan for. An unmodified headless browser is nearly as detectable as a raw HTTP client, so solving the TLS layer alone won't help if your browser environment leaks automation markers at the JavaScript level.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Session Cross-Contamination: Your Cookies Follow You to New IPs
&lt;/h3&gt;

&lt;p&gt;This is what experienced scraping engineers call the "silent killer" of proxy rotation.&lt;/p&gt;

&lt;p&gt;You rotate to a fresh IP, but your HTTP client reuses the same cookie jar. The target site issued a tracking cookie on your first request. When your second request arrives from a completely different IP but carries the same cookie, the server instantly correlates them. Worse, some sites use that cookie to retroactively flag the original IP, poisoning it for future users of the same proxy pool.&lt;/p&gt;

&lt;p&gt;Cross-contamination happens in several ways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Shared cookie jars&lt;/strong&gt; across requests that use different proxy IPs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent localStorage or sessionStorage&lt;/strong&gt; in headless browser instances that aren't properly isolated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication tokens&lt;/strong&gt; that travel with the session rather than being scoped to a specific IP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP client connection pools&lt;/strong&gt; that reuse TCP connections across proxy rotations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fix sounds simple—isolate sessions per IP—but many scraping frameworks default to connection reuse for performance. If you haven't explicitly configured session isolation, you're almost certainly leaking identity across rotations.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Your Request Patterns Don't Look Human
&lt;/h3&gt;

&lt;p&gt;Real users browse with irregular timing. They spend 15 seconds on one page, 3 seconds on the next, 45 seconds reading a long article. They follow links. They load images, CSS, and JavaScript. They scroll.&lt;/p&gt;

&lt;p&gt;Automated scrapers typically do none of this. They fire requests at fixed intervals (or as fast as possible), skip all subresources, navigate directly to deep URLs without any referrer, and maintain perfectly consistent request timing.&lt;/p&gt;

&lt;p&gt;Modern behavioral analysis engines detect these patterns reliably. Specific red flags include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Uniform request intervals&lt;/strong&gt; — A 2-second delay between every request is just as suspicious as no delay, because real humans don't operate on a metronome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High concurrency from a single IP&lt;/strong&gt; — More than 5-10 concurrent connections from one residential IP is abnormal for household traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Missing referrer chains&lt;/strong&gt; — Jumping directly to &lt;code&gt;/product/12345&lt;/code&gt; without first visiting the homepage or category page.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No subresource loading&lt;/strong&gt; — A real browser loads dozens of assets per page. A raw HTTP request loads zero.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Linear crawl patterns&lt;/strong&gt; — Systematically iterating through paginated results (page=1, page=2, page=3...) in perfect sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Your Proxy Pool Has Contaminated IPs
&lt;/h3&gt;

&lt;p&gt;Not all rotating residential proxy pools are equal. The IP you just rotated to might already be flagged because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A previous user abused it&lt;/strong&gt; — Proxy IPs are shared resources. If someone else ran aggressive scraping, spamming, or credential stuffing through that IP last week, its reputation score is already damaged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Subnet concentration&lt;/strong&gt; — Your provider's pool might be heavily concentrated in a few /24 subnets. When a site sees dozens of requests from the same subnet within an hour, it blocks the entire range.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small pool, high overlap&lt;/strong&gt; — Budget providers with pools under a few million IPs will cycle you back to previously-used (and potentially flagged) IPs quickly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stale IPs&lt;/strong&gt; — IPs that haven't been refreshed or verified recently may already appear on commercial IP reputation blocklists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We ran a quick pool quality test on a budget provider last quarter: out of 50 random IPs, 32 returned a 403 when we visited the target site through a normal Chrome browser—no scraping, no automation, just a regular page load. That's a 64% contamination rate, and no amount of configuration tuning will fix an IP that's already flagged at the network level.&lt;/p&gt;

&lt;p&gt;You can run the same test: take an IP from your pool, configure it as a manual proxy in a regular browser, and try accessing your target site. If it's blocked even with a real browser and natural browsing, the IP itself is the problem—not your configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. You're Using the Wrong Rotation Mode for Your Task
&lt;/h3&gt;

&lt;p&gt;Rotating residential proxies typically offer two modes: &lt;strong&gt;per-request rotation&lt;/strong&gt; (new IP for each request) and &lt;strong&gt;sticky sessions&lt;/strong&gt; (same IP maintained for a set duration, commonly 1-30 minutes, with some providers supporting up to 180 minutes).&lt;/p&gt;

&lt;p&gt;Using per-request rotation for stateful workflows is a common misconfiguration. If your task involves login → navigate → interact → extract, rotating the IP between each step makes you look like four different users trying to share one session. Most sites treat this as a session hijacking attempt and terminate the session immediately.&lt;/p&gt;

&lt;p&gt;The reverse also causes problems. Using a long sticky session for high-volume stateless collection means you're hammering a single IP with hundreds of requests, which triggers rate limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; stateful operations (authentication, multi-page checkouts, paginated browsing within a session) need sticky sessions. Stateless collection (scraping independent product pages, checking prices across URLs) works better with per-request rotation and properly isolated sessions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Diagnosis: Match Your Symptoms to the Root Cause
&lt;/h2&gt;

&lt;p&gt;Use the HTTP response your scraper receives to narrow down the cause:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Most Likely Cause&lt;/th&gt;
&lt;th&gt;First Step&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;403 on every IP&lt;/strong&gt;, including brand-new ones&lt;/td&gt;
&lt;td&gt;TLS fingerprint mismatch (your client doesn't look like a browser at the protocol level)&lt;/td&gt;
&lt;td&gt;Switch to &lt;code&gt;curl_cffi&lt;/code&gt; with &lt;code&gt;impersonate="chrome"&lt;/code&gt; or use Playwright/Puppeteer — see Fix #1 below&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;403 after 3-5 requests&lt;/strong&gt;, initial ones succeed&lt;/td&gt;
&lt;td&gt;Browser fingerprint correlation or cookie cross-contamination&lt;/td&gt;
&lt;td&gt;Isolate sessions per IP; rotate fingerprint profile per session — see Fix #2 and #3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;429 Too Many Requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rate limiting—you're sending too fast or with too much concurrency&lt;/td&gt;
&lt;td&gt;Reduce concurrency to 2-3 per IP; add randomized delays; respect &lt;code&gt;Retry-After&lt;/code&gt; headers — see Fix #4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CAPTCHA loops across different IPs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Behavioral analysis or fingerprint detection triggering challenges&lt;/td&gt;
&lt;td&gt;Reduce request speed; add realistic browsing patterns; verify stealth configuration — see Fix #2 and #4&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Homepage loads, but login/checkout fails&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Session inconsistency—you're rotating IPs mid-workflow where sticky sessions are needed&lt;/td&gt;
&lt;td&gt;Switch to sticky session mode for stateful operations — see Fix #6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Some IPs work, others fail immediately&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Contaminated IP pool—specific IPs are pre-flagged&lt;/td&gt;
&lt;td&gt;Test IPs manually in a real browser; switch proxy provider or request a different pool segment — see Fix #5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Everything fails on one target, works on others&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Target site's specific anti-bot policy (e.g., aggressive Cloudflare configuration)&lt;/td&gt;
&lt;td&gt;Use full browser automation; reduce concurrency significantly; verify your access complies with the site's ToS&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key diagnostic principle:&lt;/strong&gt; change only one variable at a time. If you swap your proxy provider, switch to Playwright, and add random delays all at once, you won't know which change fixed the problem—or which might cause the next one.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Fix Each Blocking Trigger
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Fix #1: TLS Fingerprint Mismatch
&lt;/h3&gt;

&lt;p&gt;You have two reliable paths:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Use a real browser engine.&lt;/strong&gt; Playwright and Puppeteer launch actual Chromium instances, which naturally produce authentic TLS fingerprints because they &lt;em&gt;are&lt;/em&gt; real browsers. This is the most reliable approach, though it consumes more memory and CPU than lightweight HTTP clients.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B: Use a TLS-impersonating HTTP library.&lt;/strong&gt; For Python, &lt;code&gt;curl_cffi&lt;/code&gt; is the go-to solution. It wraps curl-impersonate, which modifies the underlying TLS library to produce browser-matching fingerprints. The API mirrors Python's &lt;code&gt;requests&lt;/code&gt; library, so migration is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install: pip install curl_cffi
from curl_cffi import requests

proxy = "http://USERNAME:PASSWORD@proxy-host:port"
response = requests.get(
    "https://target-site.com/page",
    impersonate="chrome",
    proxies={"http": proxy, "https": proxy},
)
print(response.status_code)  # 200 instead of 403
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;impersonate="chrome"&lt;/code&gt; parameter configures curl_cffi to use the same TLS settings that a standard Chrome browser uses—cipher suites, extension ordering, and HTTP/2 configuration. Replace &lt;code&gt;USERNAME:PASSWORD@proxy-host:port&lt;/code&gt; with your actual proxy credentials from your provider's dashboard.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt; Use the JA3 fingerprint checker at Scrapfly's web scraping tools page to confirm your JA3 hash matches a recognized Chrome signature.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix #2: Browser Fingerprint Consistency
&lt;/h3&gt;

&lt;p&gt;For browser-based scraping, stealth plugins correct automation artifacts that cause headless browsers to be misclassified as bots. Minimal setup for Playwright with &lt;code&gt;playwright-stealth&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install: pip install playwright playwright-stealth
# Then: playwright install chromium
from playwright.sync_api import sync_playwright
from playwright_stealth import stealth_sync

with sync_playwright() as p:
    browser = p.chromium.launch(
        proxy={"server": "http://proxy-host:port",
               "username": "USERNAME", "password": "PASSWORD"}
    )
    context = browser.new_context(
        locale="en-US",
        timezone_id="America/New_York",  # Match your proxy IP's geo
        viewport={"width": 1920, "height": 1080}
    )
    page = context.new_page()
    stealth_sync(page)
    page.goto("https://target-site.com")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you switch to a new proxy IP, update the browser context to match: timezone, &lt;code&gt;Accept-Language&lt;/code&gt;, and locale should all align with the IP's geolocation. A US IP should have &lt;code&gt;en-US&lt;/code&gt; language and a US timezone—not &lt;code&gt;Asia/Tokyo&lt;/code&gt; carried over from a previous session.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix #3: Isolate Sessions Per IP
&lt;/h3&gt;

&lt;p&gt;Every IP rotation should start with a clean session state. In Playwright, &lt;code&gt;browser.new_context()&lt;/code&gt; creates a fully isolated environment with its own cookie store, localStorage, cache, and network state. Calling &lt;code&gt;context.close()&lt;/code&gt; wipes everything clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Each proxy IP gets its own isolated context
for proxy_ip in proxy_list:
    context = browser.new_context(
        proxy={"server": proxy_ip,
               "username": "USER", "password": "PASS"}
    )
    page = context.new_page()
    stealth_sync(page)
    page.goto("https://target-site.com/page")
    data = page.content()
    context.close()  # Cookies, storage, cache all destroyed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're using raw HTTP clients like &lt;code&gt;curl_cffi&lt;/code&gt;, instantiate a new &lt;code&gt;Session()&lt;/code&gt; object per IP rather than reusing a persistent one, and ensure HTTP connection pooling doesn't reuse TCP connections across proxy switches.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix #4: Humanize Your Request Patterns
&lt;/h3&gt;

&lt;p&gt;Replace fixed delays with randomized intervals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Base delay between requests:&lt;/strong&gt; 2-8 seconds, randomly sampled per request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Add jitter:&lt;/strong&gt; ±30% randomization on top of the base delay&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency per IP:&lt;/strong&gt; Cap at 2-5 concurrent requests per exit IP&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff on errors:&lt;/strong&gt; Start at 2-3 seconds, double on each consecutive failure, cap at 30-60 seconds. Add random jitter (AWS's architecture blog recommends "full jitter") to prevent retry storms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Request sequencing:&lt;/strong&gt; Visit the homepage first, then a category page, then the target page. Load at least some subresources. Include realistic referrer headers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fix #5: Test and Switch Your Proxy Pool
&lt;/h3&gt;

&lt;p&gt;Before blaming your configuration, verify whether the IPs themselves are clean:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take 10-20 IPs from your pool and test them manually—configure each as a browser proxy and visit your target site normally. If more than 20-30% fail even with a real browser, the pool quality is the issue, not your setup.&lt;/li&gt;
&lt;li&gt;Check geographic distribution. If most of your IPs fall into a small number of subnets, request a more diverse allocation from your provider.&lt;/li&gt;
&lt;li&gt;Test at different times of day. Some proxy pools have higher contamination during peak usage hours.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When evaluating providers, key metrics are: pool size, daily IP refresh rate, success rate monitoring, and sticky session support. For context, &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; maintains 100M+ residential IPs across 200+ regions with 100,000+ new IPs added daily, a 98.9% real-time success rate, and sticky sessions up to 180 minutes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fix #6: Match Rotation Mode to Your Task
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Stateless data collection&lt;/strong&gt; (independent URLs, price checks, SERP scraping): Use per-request rotation. Each request is self-contained, so a fresh IP per request maximizes coverage and minimizes rate-limit accumulation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateful workflows&lt;/strong&gt; (login flows, multi-step forms, paginated browsing within a session): Use sticky sessions. Set the session duration to cover your entire workflow with a safety margin—if your login → navigate → extract flow takes 3 minutes, set a 10-minute sticky session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hybrid tasks&lt;/strong&gt; (log in with sticky session, then collect data with rotation): Use a sticky session for authentication, capture the auth cookies, then switch to per-request rotation for the data collection phase—each rotated request carries the auth cookies while everything else (fingerprint, non-auth cookies) resets.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Recommended Safe Defaults
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameter&lt;/th&gt;
&lt;th&gt;Recommended Range&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency per exit IP&lt;/td&gt;
&lt;td&gt;2-5&lt;/td&gt;
&lt;td&gt;Matches realistic household browsing load&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Base delay between requests&lt;/td&gt;
&lt;td&gt;2-8 seconds (randomized)&lt;/td&gt;
&lt;td&gt;Falls within natural human browsing intervals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Jitter on delay&lt;/td&gt;
&lt;td&gt;±30%&lt;/td&gt;
&lt;td&gt;Prevents detectable timing patterns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backoff on 429/error&lt;/td&gt;
&lt;td&gt;Exponential, 2s → 60s cap, with full jitter&lt;/td&gt;
&lt;td&gt;Avoids retry storms; respects server signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sticky session duration&lt;/td&gt;
&lt;td&gt;10-30 minutes for typical workflows&lt;/td&gt;
&lt;td&gt;Covers multi-step tasks with safety margin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test batch size before scaling&lt;/td&gt;
&lt;td&gt;20-50 requests&lt;/td&gt;
&lt;td&gt;Enough to detect pool quality issues without burning budget&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Start conservative and scale up gradually. It's far easier to increase concurrency after confirming your setup works than to recover from getting your entire proxy pool flagged on a target site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP rotation alone doesn't prevent blocking.&lt;/strong&gt; Modern anti-bot systems check TLS fingerprints, browser fingerprints, session behavior, and request patterns independently. Failing any single layer triggers detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS fingerprint mismatch is the #1 cause of instant blocking.&lt;/strong&gt; If you're using Python &lt;code&gt;requests&lt;/code&gt; or similar HTTP libraries, your TLS handshake identifies you as a bot before any HTML loads. Switch to &lt;code&gt;curl_cffi&lt;/code&gt; with browser impersonation or use a real browser engine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Diagnose before you fix.&lt;/strong&gt; Use the HTTP status code and failure pattern to identify which detection layer you're failing, then apply the targeted fix. Changing everything at once wastes time and obscures what actually works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Isolate everything per IP.&lt;/strong&gt; Cookies, sessions, fingerprint profiles, and browser contexts should all reset when you rotate to a new IP. Cross-contamination is the most overlooked cause of proxy blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Start conservative.&lt;/strong&gt; 2-5 concurrent requests per IP, 2-8 second randomized delays, and exponential backoff on errors. Scale up only after verifying stability.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Disclosure: Our team works with Proxy001. We recommend them based on hands-on testing, but encourage you to evaluate any provider against your specific targets before committing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Need a residential proxy pool built for reliability at scale?&lt;/strong&gt; &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; offers 100M+ rotating residential IPs, a 98.9% success rate, sticky sessions up to 180 minutes, and SDKs for Python, Node.js, Puppeteer, and Selenium. &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Start your free proxy test →&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>networking</category>
      <category>python</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>2026 Zero-Leak Docker + Residential Proxy Guide</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Thu, 26 Feb 2026 01:26:44 +0000</pubDate>
      <link>https://dev.to/miller_proxy/2026-zero-leak-docker-residential-proxy-guide-51i</link>
      <guid>https://dev.to/miller_proxy/2026-zero-leak-docker-residential-proxy-guide-51i</guid>
      <description>&lt;p&gt;&lt;em&gt;By [Author Name] | Infrastructure &amp;amp; proxy automation, [X] years | Last tested on Ubuntu 24.04 LTS, Docker 27.5, Puppeteer 23.x, Playwright 1.50&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Last Updated: February 2026 | Next Review: August 2026&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You configured your Docker container to route traffic through a residential proxy. You ran &lt;code&gt;curl ifconfig.me&lt;/code&gt; from inside the container and saw the proxy IP. Everything looked clean — until Cloudflare served you a challenge page, or worse, your real IP showed up in the target's access logs.&lt;/p&gt;

&lt;p&gt;The gap between "proxy configured" and "zero-leak" is where most setups fail. A Docker container on a default bridge network has multiple egress paths, and not all of them respect your &lt;code&gt;HTTP_PROXY&lt;/code&gt; environment variable. DNS queries can slip out through the host resolver. If you are running a headless browser for tasks like ad verification or price monitoring, WebRTC STUN requests can expose your origin IP even when every other channel is locked down.&lt;/p&gt;

&lt;p&gt;This guide closes every one of those gaps. By the end, you will have a docker-compose.yml that forces all container traffic through a residential proxy, iptables rules that kill connectivity if the proxy drops, DNS locked to the proxy tunnel, WebRTC disabled at the browser level, and a verification script that proves all four layers are working.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before you start, confirm you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Docker Engine 24.0+&lt;/strong&gt; and &lt;strong&gt;Docker Compose V2&lt;/strong&gt; (run &lt;code&gt;docker --version&lt;/code&gt; and &lt;code&gt;docker compose version&lt;/code&gt; to check)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Linux host&lt;/strong&gt; with iptables support (Ubuntu 22.04+, Debian 12+, or equivalent — also works under WSL2 on Windows with iptables enabled)&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;residential proxy account&lt;/strong&gt; with endpoint credentials (host, port, username, password) — you will need both HTTP and SOCKS5 endpoints if your workflow includes headless browsers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root or sudo access&lt;/strong&gt; on the host (required for iptables rules)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you do not have a residential proxy account yet, any provider that offers rotating and sticky sessions over both HTTP and SOCKS5 will work. The configuration examples later in this guide use a generic placeholder format (&lt;code&gt;proxy.yourprovider.com:port&lt;/code&gt;) — replace with your actual credentials.&lt;/p&gt;
&lt;h2&gt;
  
  
  How Docker Leaks Your Real IP: Three Vectors
&lt;/h2&gt;

&lt;p&gt;Understanding &lt;em&gt;where&lt;/em&gt; leaks happen is the prerequisite for plugging them. A Docker container on a &lt;a href="https://docs.docker.com/engine/network/drivers/bridge/" rel="noopener noreferrer"&gt;user-defined bridge network&lt;/a&gt; routes outbound traffic through the host's network stack via masquerading (SNAT). The host kernel assigns an ephemeral source port and forwards the packet out through the default gateway. This is the normal path, and it has three distinct leak vectors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector 1: Direct TCP/HTTP egress.&lt;/strong&gt; Setting &lt;code&gt;HTTP_PROXY&lt;/code&gt; and &lt;code&gt;HTTPS_PROXY&lt;/code&gt; as environment variables only works if the application inside the container honors those variables. Most HTTP client libraries (Python &lt;code&gt;requests&lt;/code&gt;, Node &lt;code&gt;axios&lt;/code&gt;) do. But lower-level tools, raw socket connections, and some automation frameworks ignore them entirely. Any request that skips the proxy exits through the host's real IP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector 2: DNS resolution.&lt;/strong&gt; On a user-defined bridge network, Docker runs an embedded DNS server at &lt;code&gt;127.0.0.11&lt;/code&gt; that handles container-to-container name resolution. But external domain lookups get forwarded to whatever DNS servers the host is configured with — typically your ISP's resolvers or a public resolver like &lt;code&gt;8.8.8.8&lt;/code&gt;. Those DNS queries leave from the host's real IP, revealing which domains your container is resolving. Even if your HTTP traffic goes through the proxy, the DNS trail exposes your activity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vector 3: WebRTC STUN requests.&lt;/strong&gt; If your container runs a headless Chromium instance (Puppeteer, Playwright, Selenium), the browser's WebRTC stack sends STUN requests to discover the machine's external IP address. These STUN requests use UDP and bypass HTTP proxy settings entirely. A target site that checks WebRTC will see your real IP alongside the proxy IP — an instant red flag.&lt;/p&gt;

&lt;p&gt;A zero-leak setup must close all three vectors simultaneously. If you only fix one, the other two still expose you.&lt;/p&gt;

&lt;p&gt;We learned this the hard way. Our initial price-monitoring stack had &lt;code&gt;HTTP_PROXY&lt;/code&gt; set and passed a basic &lt;code&gt;curl&lt;/code&gt; check from inside the container — the proxy IP came back as expected. Three days in, we noticed our host IP appearing in Cloudflare's firewall event logs on a target site. It turned out the Puppeteer container's WebRTC STUN requests had been leaking our origin the entire time, and we had no idea until the target started blocking us.&lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The zero-leak stack has four layers, each blocking a specific leak vector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────┐
│  SCRAPER CONTAINER                          │
│  ┌───────────────────────────────────────┐  │
│  │ App (Puppeteer / Python / Node)       │  │
│  │  • HTTP_PROXY + HTTPS_PROXY set       │  │ ← Layer 1: Proxy routing
│  │  • WebRTC disabled via init script    │  │ ← Layer 4: Fingerprint
│  └───────────────────────────────────────┘  │
│  DNS: locked to proxy-safe resolver (DoH)   │ ← Layer 2: DNS lock
├─────────────────────────────────────────────┤
│  HOST iptables (DOCKER-USER chain)          │
│  ALLOW → proxy IP + DoH DNS only            │ ← Layer 3: Kill switch
│  DROP  → everything else                    │
└─────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: docker-compose.yml with Residential Proxy
&lt;/h2&gt;

&lt;p&gt;Create a project directory and add this &lt;code&gt;docker-compose.yml&lt;/code&gt;. This example uses a Python-based scraper container, but the proxy configuration applies to any image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scraper&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.12-slim&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scraper-zero-leak&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Replace with your residential proxy credentials&lt;/span&gt;
      &lt;span class="na"&gt;HTTP_PROXY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://USER:PASS@proxy.yourprovider.com:PORT"&lt;/span&gt;
      &lt;span class="na"&gt;HTTPS_PROXY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://USER:PASS@proxy.yourprovider.com:PORT"&lt;/span&gt;
      &lt;span class="na"&gt;ALL_PROXY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;   &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;socks5h://USER:PASS@proxy.yourprovider.com:SOCKS_PORT"&lt;/span&gt;
      &lt;span class="na"&gt;NO_PROXY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost,127.0.0.1"&lt;/span&gt;
    &lt;span class="na"&gt;dns&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;1.1.1.1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;1.0.0.1&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;isolated&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./app:/app&lt;/span&gt;
    &lt;span class="na"&gt;working_dir&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;isolated&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
    &lt;span class="na"&gt;internal&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key points in this configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ALL_PROXY&lt;/code&gt; with &lt;code&gt;socks5h://&lt;/code&gt;&lt;/strong&gt;: The &lt;code&gt;h&lt;/code&gt; suffix tells the client to send DNS queries through the SOCKS5 proxy as well, not resolve them locally. This is critical — &lt;code&gt;socks5://&lt;/code&gt; (without &lt;code&gt;h&lt;/code&gt;) resolves DNS on the host side, which leaks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IP whitelist auth alternative&lt;/strong&gt;: If your provider uses IP whitelist authentication instead of username/password, first add your server's public IP in your provider's dashboard, then use credentials without the &lt;code&gt;USER:PASS@&lt;/code&gt; prefix: &lt;code&gt;"http://proxy.yourprovider.com:PORT"&lt;/code&gt;. The proxy authenticates by recognizing your server's IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;NO_PROXY&lt;/code&gt;&lt;/strong&gt;: Only excludes localhost. Do not add your target domains here — that would bypass the proxy for exactly the traffic you need to protect.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dns&lt;/code&gt; directive&lt;/strong&gt;: Overrides the default DNS inherited from the host. We set Cloudflare's &lt;code&gt;1.1.1.1&lt;/code&gt; here as a fallback for non-SOCKS5h traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-defined &lt;code&gt;isolated&lt;/code&gt; network&lt;/strong&gt;: Avoids the default bridge network, giving you more control over routing and iptables filtering.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Choosing Between Rotating and Sticky Sessions
&lt;/h3&gt;

&lt;p&gt;Most residential proxy providers let you control session behavior through the username field or a session parameter in the endpoint URL. The right choice depends on your task:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotating proxy&lt;/strong&gt; (new IP per request): Use for high-volume data collection where each request is independent — price monitoring across thousands of product pages, search result sampling, ad verification across geos.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky session&lt;/strong&gt; (same IP for a set duration): Use when you need session continuity — navigating multi-page flows, maintaining login state, or any task where consecutive requests from different IPs would trigger security flags.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Configure this in your proxy credentials. For example, many providers use a format like &lt;code&gt;user-session-abc123:pass@endpoint&lt;/code&gt; for sticky or &lt;code&gt;user-rotate:pass@endpoint&lt;/code&gt; for rotating. &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt;, for instance, supports sticky sessions up to 60 minutes with automatic rotation as the default — see their &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt; for the exact endpoint format. Check your provider's docs for the specific parameter syntax.&lt;/p&gt;

&lt;p&gt;Expect higher latency than datacenter proxies — benchmark your endpoint and set timeouts accordingly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Lock Down DNS
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;dns&lt;/code&gt; directive in docker-compose.yml sets the container's &lt;code&gt;/etc/resolv.conf&lt;/code&gt; to point at the specified servers. But this alone is not sufficient — the DNS queries to &lt;code&gt;1.1.1.1&lt;/code&gt; still leave from the host's real IP unless they travel through the proxy.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;socks5h://&lt;/code&gt; protocol in &lt;code&gt;ALL_PROXY&lt;/code&gt; handles this for SOCKS5-aware applications: DNS resolution happens at the proxy server, not locally. But not every tool in your container will use &lt;code&gt;ALL_PROXY&lt;/code&gt;. For defense in depth, add a second layer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Force DNS through the SOCKS5 proxy at the OS level.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Add an entrypoint script that configures the container's DNS to route through the proxy tunnel:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# entrypoint.sh — run as root before dropping to app user&lt;/span&gt;

&lt;span class="c"&gt;# Point DNS at localhost; we'll tunnel it through the proxy&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"nameserver 127.0.0.1"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/resolv.conf

&lt;span class="c"&gt;# Start a lightweight DNS-over-HTTPS forwarder&lt;/span&gt;
&lt;span class="c"&gt;# dnsproxy is a ~5MB static binary from AdguardTeam (https://github.com/AdguardTeam/dnsproxy)&lt;/span&gt;
/usr/local/bin/dnsproxy &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upstream&lt;/span&gt; &lt;span class="s2"&gt;"https://1.1.1.1/dns-query"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bootstrap&lt;/span&gt; &lt;span class="s2"&gt;"1.1.1.1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--listen&lt;/span&gt; &lt;span class="s2"&gt;"127.0.0.1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 53 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--all-servers&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &amp;amp;

&lt;span class="c"&gt;# Hand off to the main application&lt;/span&gt;
&lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update your docker-compose.yml to use this entrypoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;scraper&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# ... previous config ...&lt;/span&gt;
    &lt;span class="na"&gt;entrypoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/app/entrypoint.sh"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;main.py"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You will need to include &lt;code&gt;dnsproxy&lt;/code&gt; in your container image. Add this to your Dockerfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="c"&gt;# Pin a known version — check https://github.com/AdguardTeam/dnsproxy/releases for latest&lt;/span&gt;
&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; DNSPROXY_VERSION=v0.79.0&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; wget &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; wget &lt;span class="nt"&gt;-qO&lt;/span&gt; /tmp/dnsproxy.tar.gz &lt;span class="se"&gt;\
&lt;/span&gt;       &lt;span class="s2"&gt;"https://github.com/AdguardTeam/dnsproxy/releases/download/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DNSPROXY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/dnsproxy-linux-amd64-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DNSPROXY_VERSION&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; /tmp/dnsproxy.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /tmp/ &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;mv&lt;/span&gt; /tmp/linux-amd64/dnsproxy /usr/local/bin/dnsproxy &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;chmod&lt;/span&gt; +x /usr/local/bin/dnsproxy &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /tmp/dnsproxy.tar.gz /tmp/linux-amd64 &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get purge &lt;span class="nt"&gt;-y&lt;/span&gt; wget &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get autoremove &lt;span class="nt"&gt;-y&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app/ /app/&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B: Rely on &lt;code&gt;socks5h://&lt;/code&gt; and iptables.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If your application exclusively uses &lt;code&gt;socks5h://&lt;/code&gt; for all connections, DNS is already tunneled. Combine this with the iptables kill switch (next step), which blocks any DNS packet that tries to leave directly. This approach requires no additional software but assumes all tools in the container respect &lt;code&gt;ALL_PROXY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Option A is more robust. Option B is simpler. For production workloads where a DNS leak could compromise the operation, use Option A.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compatibility note:&lt;/strong&gt; If you use Option A with the iptables kill switch in Step 3, you must add whitelist rules for &lt;code&gt;1.1.1.1&lt;/code&gt; and &lt;code&gt;1.0.0.1&lt;/code&gt; on port 443 — otherwise the kill switch blocks &lt;code&gt;dnsproxy&lt;/code&gt;'s outbound DoH requests and DNS fails. Step 3 includes these rules with an explanation of the trade-off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: iptables Kill Switch
&lt;/h2&gt;

&lt;p&gt;This is the most important safety net. If the proxy endpoint goes down, if credentials expire, or if any application in the container ignores proxy settings and tries to connect directly, the kill switch ensures the traffic is &lt;strong&gt;dropped&lt;/strong&gt;, not routed through the host's real IP.&lt;/p&gt;

&lt;p&gt;The rules go in the &lt;code&gt;DOCKER-USER&lt;/code&gt; chain, which Docker evaluates before its own forwarding rules. You need the container's subnet and the proxy server's IP address.&lt;/p&gt;

&lt;p&gt;First, find your container's network subnet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network inspect isolated &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{range .IPAM.Config}}{{.Subnet}}{{end}}'&lt;/span&gt;
&lt;span class="c"&gt;# Example output: 172.20.0.0/16&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then apply the rules (replace &lt;code&gt;PROXY_IP&lt;/code&gt; with your residential proxy endpoint's resolved IP, and &lt;code&gt;SUBNET&lt;/code&gt; with the output above):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Allow container traffic ONLY to the proxy endpoint&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; PROXY_IP &lt;span class="nt"&gt;-j&lt;/span&gt; RETURN

&lt;span class="c"&gt;# Allow DNS-over-HTTPS to Cloudflare (required for Option A's dnsproxy)&lt;/span&gt;
&lt;span class="c"&gt;# This opens a narrow egress path, but only for encrypted DNS — no plaintext leaks&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; 1.1.1.1 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 443 &lt;span class="nt"&gt;-j&lt;/span&gt; RETURN
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; 1.0.0.1 &lt;span class="nt"&gt;-p&lt;/span&gt; tcp &lt;span class="nt"&gt;--dport&lt;/span&gt; 443 &lt;span class="nt"&gt;-j&lt;/span&gt; RETURN

&lt;span class="c"&gt;# Allow DNS to the container's internal resolver (for Option A above)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; 127.0.0.0/8 &lt;span class="nt"&gt;-j&lt;/span&gt; RETURN

&lt;span class="c"&gt;# Allow container-to-container communication within the subnet&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-j&lt;/span&gt; RETURN

&lt;span class="c"&gt;# Drop everything else from the container subnet&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-A&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why the DoH whitelist rules are needed:&lt;/strong&gt; If you use DNS Option A from Step 2, &lt;code&gt;dnsproxy&lt;/code&gt; inside the container sends DNS-over-HTTPS requests to &lt;code&gt;1.1.1.1&lt;/code&gt; on port 443. Without these two rules, the kill switch would block those requests and DNS resolution would fail entirely. The trade-off is a narrow, encrypted-only egress path to Cloudflare's DNS — it does not expose plaintext DNS queries or any other traffic. If you use DNS Option B instead (relying on &lt;code&gt;socks5h://&lt;/code&gt;), you can omit these two rules for a stricter kill switch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works:&lt;/strong&gt; The &lt;code&gt;DOCKER-USER&lt;/code&gt; chain is processed before Docker's own &lt;code&gt;DOCKER-FORWARD&lt;/code&gt; chain (see &lt;a href="https://docs.docker.com/engine/network/packet-filtering-firewalls/" rel="noopener noreferrer"&gt;Docker packet filtering docs&lt;/a&gt;). The &lt;code&gt;-I&lt;/code&gt; (insert) flag puts RETURN rules at the top of the chain; the &lt;code&gt;-A&lt;/code&gt; (append) flag puts the DROP at the bottom. If you append DROP before inserting RETURN rules, the container loses all connectivity — rule order matters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Persist the rules across reboots:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; iptables-persistent
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables-save &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/iptables/rules.v4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important caveat:&lt;/strong&gt; If your residential proxy endpoint resolves to multiple IPs (common with large providers that use DNS-based load balancing), you need to whitelist all of them or whitelist the entire IP range. Run &lt;code&gt;dig +short proxy.yourprovider.com&lt;/code&gt; to check, and add a rule for each IP or use a CIDR range.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Browser-Level Protection
&lt;/h2&gt;

&lt;p&gt;If your container runs a headless browser, you need to neutralize WebRTC at the application level. Network-level iptables rules help (they block outbound UDP to STUN servers), but the definitive fix is preventing the browser from attempting WebRTC at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Puppeteer (Node.js)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;puppeteer&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;puppeteer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;headless&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;new&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--proxy-server=socks5://proxy.yourprovider.com:SOCKS_PORT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE proxy.yourprovider.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Match fingerprint to proxy geo (adjust for your exit country)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;emulateTimezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;America/Chicago&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setExtraHTTPHeaders&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Accept-Language&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US,en;q=0.9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Disable WebRTC before any page loads&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluateOnNewDocument&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Remove RTCPeerConnection to prevent STUN/TURN requests&lt;/span&gt;
  &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;defineProperty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;mediaDevices&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;getUserMedia&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RTCPeerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitRTCPeerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MediaStreamTrack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PROXY_USER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://browserleaks.com/webrtc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--host-resolver-rules&lt;/code&gt; argument forces Chrome to resolve DNS through the SOCKS5 proxy rather than the system resolver. Combined with the &lt;code&gt;evaluateOnNewDocument&lt;/code&gt; script that nullifies &lt;code&gt;RTCPeerConnection&lt;/code&gt;, this eliminates both DNS and WebRTC leak vectors at the browser level.&lt;/p&gt;

&lt;h3&gt;
  
  
  Playwright (Node.js / Python)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;playwright&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chromium&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;socks5://proxy.yourprovider.com:SOCKS_PORT&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PROXY_USER&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PROXY_PASS&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE proxy.yourprovider.com&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newContext&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;locale&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;             &lt;span class="c1"&gt;// Match proxy geo&lt;/span&gt;
  &lt;span class="na"&gt;timezoneId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;America/Chicago&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// Match proxy geo&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Disable WebRTC via init script&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addInitScript&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RTCPeerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitRTCPeerConnection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MediaStreamTrack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mediaDevices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;newPage&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://browserleaks.com/webrtc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Playwright's &lt;code&gt;addInitScript&lt;/code&gt; runs before any page scripts, making it the reliable injection point.&lt;/p&gt;

&lt;h3&gt;
  
  
  Selenium (Python)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;selenium.webdriver.chrome.options&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Options&lt;/span&gt;

&lt;span class="n"&gt;opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Options&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--headless=new&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--proxy-server=socks5://proxy.yourprovider.com:SOCKS_PORT&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;--host-resolver-rules=MAP * ~NOTFOUND, EXCLUDE proxy.yourprovider.com&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Disable WebRTC via Chrome preference
&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_experimental_option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;prefs&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webrtc.ip_handling_policy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;disable_non_proxied_udp&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webrtc.multiple_routes_enabled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;webrtc.nonproxied_udp_enabled&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;driver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;webdriver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Chrome&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;driver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;https://browserleaks.com/webrtc&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Selenium supports setting Chrome preferences directly through &lt;code&gt;add_experimental_option&lt;/code&gt;, which is a cleaner approach than JavaScript injection for this specific case. The &lt;code&gt;disable_non_proxied_udp&lt;/code&gt; policy tells Chrome to only use UDP through the configured proxy, effectively preventing WebRTC from bypassing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Python &lt;code&gt;requests&lt;/code&gt; / &lt;code&gt;curl_cffi&lt;/code&gt; (No Browser)
&lt;/h3&gt;

&lt;p&gt;If your scraper uses Python's &lt;code&gt;requests&lt;/code&gt; library or &lt;code&gt;curl_cffi&lt;/code&gt; without a browser, WebRTC is not a concern — these HTTP clients have no WebRTC stack. But you still need to ensure DNS goes through the proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;curl_cffi.requests&lt;/span&gt;

&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;curl_cffi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;impersonate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;chrome136&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.ipify.org?format=json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;proxies&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;socks5h://USER:PASS@proxy.yourprovider.com:SOCKS_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;socks5h://USER:PASS@proxy.yourprovider.com:SOCKS_PORT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/lexiforest/curl_cffi" rel="noopener noreferrer"&gt;&lt;code&gt;curl_cffi&lt;/code&gt;&lt;/a&gt; replicates browser TLS/JA3 and HTTP/2 fingerprints — it presents a genuine Chrome TLS handshake to the target server rather than the default Python client fingerprint, which many anti-bot systems flag immediately. Use the &lt;code&gt;impersonate&lt;/code&gt; parameter to match a specific browser version. The library supports versions up to Chrome 142 as of early 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fingerprint Consistency
&lt;/h3&gt;

&lt;p&gt;Disabling WebRTC stops your real IP from leaking. But if the rest of your request fingerprint contradicts the proxy IP's geography or device profile, anti-bot systems will still flag the mismatch — not as a leak, but as an anomaly. These signals need to be internally consistent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;What to Match&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Timezone&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must match proxy IP geolocation&lt;/td&gt;
&lt;td&gt;Playwright: &lt;code&gt;timezoneId&lt;/code&gt; in context. Puppeteer: &lt;code&gt;--timezone=America/Chicago&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Locale / Accept-Language&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must match proxy IP country&lt;/td&gt;
&lt;td&gt;Set &lt;code&gt;Accept-Language: en-US,en;q=0.9&lt;/code&gt; for US proxies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User-Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must match a real, current browser version&lt;/td&gt;
&lt;td&gt;Use the same version string as your &lt;code&gt;curl_cffi&lt;/code&gt; impersonate target or your Chromium binary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Screen resolution&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must be a real device resolution&lt;/td&gt;
&lt;td&gt;Playwright: &lt;code&gt;viewport&lt;/code&gt; in context. Avoid exotic sizes like 1x1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TLS fingerprint (JA3)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Must match the claimed User-Agent browser&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl_cffi&lt;/code&gt; handles this automatically. For headless browsers, use a current Chromium build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;WebGL renderer&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Should not reveal a headless-only GPU string&lt;/td&gt;
&lt;td&gt;Headless Chromium defaults to SwiftShader, which anti-bot systems associate with automated browsers. Launch with &lt;code&gt;--use-gl=angle --use-angle=swiftshader-webgl&lt;/code&gt; so the renderer string matches what a normal desktop Chrome would report. If the target still misclassifies your traffic, consider running headed Chromium inside an Xvfb virtual display instead.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most common mistake: using a US residential proxy IP while the browser's timezone is set to &lt;code&gt;UTC&lt;/code&gt; and the Accept-Language header says &lt;code&gt;zh-CN&lt;/code&gt;. Each mismatch is a detection signal. Keep them coherent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Verify Zero-Leak Status
&lt;/h2&gt;

&lt;p&gt;Configuration without verification is guesswork. Run these four checks from inside your container after setup.&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 1: IP Address
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;scraper-zero-leak curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://api.ipify.org?format&lt;span class="o"&gt;=&lt;/span&gt;json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected:&lt;/strong&gt; Returns the proxy IP, not your host's IP. If you see your real IP, the &lt;code&gt;HTTP_PROXY&lt;/code&gt;/&lt;code&gt;HTTPS_PROXY&lt;/code&gt; variables are not being respected by &lt;code&gt;curl&lt;/code&gt;. Check that the proxy endpoint is reachable from within the container.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[Screenshot: terminal output showing &lt;code&gt;{"ip":"&amp;lt;proxy-ip&amp;gt;"}&lt;/code&gt; — replace with your actual test result]&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 2: DNS Leak
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;scraper-zero-leak python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import socket
result = socket.getaddrinfo('whoami.ds.akahelp.net', 80)
print(result[0][4][0])
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If DNS is tunneled through the proxy, the resolution will happen at the proxy server's location. Cross-reference the resolved IP with your proxy's expected exit region. For a more thorough test, use a DNS leak test service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec &lt;/span&gt;scraper-zero-leak curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://www.dnsleaktest.com/test/standard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Check 3: WebRTC Leak (Browser-Based)
&lt;/h3&gt;

&lt;p&gt;If your container runs a headless browser, navigate to &lt;a href="https://browserleaks.com/webrtc" rel="noopener noreferrer"&gt;browserleaks.com/webrtc&lt;/a&gt; and parse the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Inside your Puppeteer/Playwright script&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://browserleaks.com/webrtc&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;leakResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;$eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#webrtc-leak-test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;WebRTC result:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;leakResult&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Should show "No Leak" or only the proxy IP&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;[Screenshot: browserleaks.com/webrtc showing "No Leak" with only the proxy IP visible — replace with your actual test result]&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Check 4: Kill Switch Verification
&lt;/h3&gt;

&lt;p&gt;Temporarily block the proxy endpoint to simulate a proxy failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# On the host, temporarily block the proxy IP&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; PROXY_IP &lt;span class="nt"&gt;-j&lt;/span&gt; DROP

&lt;span class="c"&gt;# Try to reach the internet from the container&lt;/span&gt;
docker &lt;span class="nb"&gt;exec &lt;/span&gt;scraper-zero-leak curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;--max-time&lt;/span&gt; 5 https://api.ipify.org
&lt;span class="c"&gt;# Expected: curl times out with exit code 28, no response&lt;/span&gt;

&lt;span class="c"&gt;# Remove the test rule&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-D&lt;/span&gt; DOCKER-USER &lt;span class="nt"&gt;-s&lt;/span&gt; 172.20.0.0/16 &lt;span class="nt"&gt;-d&lt;/span&gt; PROXY_IP &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;curl&lt;/code&gt; returns any IP address during this test, your kill switch is not working.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;[Screenshot: terminal output showing &lt;code&gt;curl: (28) Connection timed out&lt;/code&gt; with exit code 28 — replace with your actual test result]&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely Cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container has no internet at all&lt;/td&gt;
&lt;td&gt;iptables rules are blocking the proxy IP&lt;/td&gt;
&lt;td&gt;Run &lt;code&gt;sudo iptables -L DOCKER-USER -n -v&lt;/code&gt; and verify the RETURN rule for your proxy IP is listed &lt;em&gt;before&lt;/em&gt; the DROP rule. Rule order matters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt; works but browser requests fail&lt;/td&gt;
&lt;td&gt;Browser is not using proxy env vars (Chromium ignores &lt;code&gt;HTTP_PROXY&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Pass the proxy via &lt;code&gt;--proxy-server&lt;/code&gt; launch argument instead of relying on environment variables.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;403 or Cloudflare challenge pages&lt;/td&gt;
&lt;td&gt;Fingerprint mismatch (timezone, locale, TLS) or low-reputation IP&lt;/td&gt;
&lt;td&gt;Verify fingerprint consistency from Step 4. Try a different proxy exit node.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DNS resolution fails inside container&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;resolv.conf&lt;/code&gt; overwritten by Docker or &lt;code&gt;dnsproxy&lt;/code&gt; not running&lt;/td&gt;
&lt;td&gt;Check &lt;code&gt;docker exec scraper-zero-leak cat /etc/resolv.conf&lt;/code&gt;. If using Option A, verify &lt;code&gt;dnsproxy&lt;/code&gt; process is alive with &lt;code&gt;docker exec scraper-zero-leak ps aux&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WebRTC still shows real IP&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;evaluateOnNewDocument&lt;/code&gt; / &lt;code&gt;addInitScript&lt;/code&gt; not executing before page load&lt;/td&gt;
&lt;td&gt;Ensure the WebRTC disable script runs before &lt;code&gt;page.goto()&lt;/code&gt;. In Playwright, use &lt;code&gt;context.addInitScript()&lt;/code&gt; (not &lt;code&gt;page.addInitScript()&lt;/code&gt;) to guarantee it runs on every new page.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requests timeout intermittently&lt;/td&gt;
&lt;td&gt;Proxy provider rate limiting or residential IP pool congestion&lt;/td&gt;
&lt;td&gt;Increase request intervals. Switch from rotating to sticky sessions for sequential page loads. Check your provider's dashboard for usage limits.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Compliance Note
&lt;/h2&gt;

&lt;p&gt;The techniques in this guide are designed for legitimate automation tasks: price monitoring of publicly listed products, ad verification across geos, SEO auditing, academic research, and quality assurance testing. Always respect the target website's &lt;code&gt;robots.txt&lt;/code&gt; and terms of service. Keep request rates under 1–3 RPS per IP.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting It Together
&lt;/h2&gt;

&lt;p&gt;The zero-leak stack works as four interlocking layers: proxy routing catches application traffic, DNS locking prevents resolver leaks, the iptables kill switch eliminates fallback-to-direct as a failure mode, and WebRTC/fingerprint controls close browser-level gaps. Removing any single layer re-opens a leak vector.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: This guide contains a referral link to Proxy001. We recommend them based on hands-on use in our own automation workflows, but the zero-leak configuration in this guide works with any residential proxy provider that supports HTTP and SOCKS5.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you need a residential proxy provider for this setup, &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;Proxy001&lt;/a&gt; offers both rotating and sticky residential sessions over HTTP and SOCKS5 across 200+ countries, with a pool of over 100 million IPs. Their Unlimited Residential plan — priced at $100/day or $3,000/month — is built specifically for high-volume automation and AI data collection workloads, with no per-GB metering that could make costs unpredictable at scale. Integration supports Python, Node.js, Puppeteer, and Selenium. You can test the service with their free trial before committing to a plan — sign up at &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;proxy001.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>networking</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Node Unblocker 2025: Complete Web Scraping Proxy Setup Guide</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Thu, 29 Jan 2026 09:12:25 +0000</pubDate>
      <link>https://dev.to/miller_proxy/node-unblocker-2025-complete-web-scraping-proxy-setup-guide-4059</link>
      <guid>https://dev.to/miller_proxy/node-unblocker-2025-complete-web-scraping-proxy-setup-guide-4059</guid>
      <description>&lt;h3&gt;
  
  
  Understanding Node Unblocker: A Lightweight Proxy Tool for Developers
&lt;/h3&gt;

&lt;p&gt;Node Unblocker serves as a specialized Node.js library that enables proxying and real-time rewriting of remote web content. At its core, this tool establishes a proxy server instance directly on your machine, making it particularly valuable for circumventing geographic restrictions and various access barriers.&lt;/p&gt;

&lt;p&gt;What exactly does this library do? Constructed on top of the widely-adopted Express framework, Node Unblocker empowers users to build their own web proxy infrastructure. Similar to conventional proxy servers, it captures outgoing HTTP requests from your device, forwards them to the destination server, and returns the response back to you.&lt;/p&gt;

&lt;p&gt;A key advantage of Node Unblocker lies in its minimal setup requirements. With just a handful of code lines, you can launch a functional proxy instance on virtually any compatible system. Beyond basic request forwarding, this tool automatically transforms URLs by prepending a &lt;code&gt;/proxy/&lt;/code&gt; path segment to the original address. This URL modification technique can occasionally help bypass local network filtering mechanisms.&lt;/p&gt;

&lt;p&gt;Since web scraping operations frequently depend on proxies to prevent detection and avoid IP blocking, Node Unblocker has gained popularity among developers—particularly those capable of deploying it on external servers or cloud infrastructure. When installed on a remote machine, you essentially create a dedicated proxy endpoint customized for your data collection requirements.&lt;/p&gt;

&lt;p&gt;That said, this tool has its boundaries. Node Unblocker may encounter difficulties when processing sophisticated modern websites. Sites that heavily utilize &lt;code&gt;postMessage&lt;/code&gt; for cross-window communication (prevalent on social media platforms), or those implementing complex AJAX requests and OAuth authentication flows, might not function correctly through the proxy.&lt;/p&gt;




&lt;h3&gt;
  
  
  How Node Unblocker Functions Under the Hood
&lt;/h3&gt;

&lt;p&gt;As previously mentioned, Node Unblocker creates a web proxy server on the host machine. Its fundamental purpose is handling the HTTP/HTTPS traffic between your device (or the hosting server) and target websites.&lt;/p&gt;

&lt;p&gt;While adequate as a straightforward proxy solution, Node Unblocker delivers substantial value through its middleware extension system—especially beneficial for users lacking access to diverse proxy pools. Without taking advantage of these customization capabilities, the tool's usefulness may be limited if you already employ comprehensive proxy solutions such as geographically distributed residential proxies.&lt;/p&gt;

&lt;p&gt;The extensive customization options are available through Node Unblocker's middleware architecture. The particular middleware components you select will largely depend on your data gathering objectives, though several commonly-used features deserve attention:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Security Policy Removal&lt;/strong&gt;: Stripping CSP headers prevents complications when proxied content attempts to interact with external domains, which could otherwise disrupt proxy functionality. This also permits inline script execution—useful for pages that dynamically load content via JavaScript.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cookie Session Handling&lt;/strong&gt;: Appropriate cookie management proves essential for preserving user sessions, navigating multi-step workflows (such as authentication or checkout processes), and can help reduce the likelihood of being blocked by target websites.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Redirect Processing&lt;/strong&gt;: Middleware ensures that HTTP redirects are properly followed through the proxy, preventing requests from inadvertently bypassing the proxy layer.&lt;/p&gt;

&lt;p&gt;Middleware excels at modifying request and response parameters—operations typically restricted by conventional proxy providers. Through Node Unblocker, you obtain granular control over components like request headers, establishing it as a versatile instrument for web scraping and similar activities.&lt;/p&gt;

&lt;p&gt;Additionally, configuration parameters enable deeper adjustments to proxy behavior. For instance, while Node Unblocker defaults to routing client-side JavaScript through the proxy, this can be deactivated when specific situations demand it.&lt;/p&gt;




&lt;h3&gt;
  
  
  Prerequisites: Setting Up Your Environment
&lt;/h3&gt;

&lt;p&gt;Starting from scratch requires several components before working with Node Unblocker:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Node.js Runtime&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The primary requirement is having the Node.js runtime environment installed on your system. After all, Node Unblocker operates as a Node.js library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Code Editor or Integrated Development Environment&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Although a basic text editor works, an IDE optimized for web development significantly improves coding efficiency. Popular options include Visual Studio Code, WebStorm, or Atom. This guide assumes you have a standard development setup ready.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Cloud Server Provider (Recommended)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Operating Node Unblocker locally means all requests still originate from your personal IP address. For effective web scraping or bypassing geographic limitations, deploying on a remote server (such as a cloud VM) is strongly advised. Typically, you would verify your configuration locally before proceeding with cloud deployment.&lt;/p&gt;




&lt;h3&gt;
  
  
  Installing Dependencies and Initializing the Project
&lt;/h3&gt;

&lt;p&gt;With your editor prepared, open the terminal or system command line. Navigate to your intended project directory and initialize a fresh Node.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-y&lt;/code&gt; flag accepts all default configuration options. You may omit it to customize details like package name or version number, though these are primarily metadata and not critical for basic setups.&lt;/p&gt;

&lt;p&gt;Next, install the required packages—the Node Unblocker library and Express framework:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;unblocker express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command downloads both packages and adds them to your project dependencies. It also generates a &lt;code&gt;node_modules&lt;/code&gt; directory and updates your &lt;code&gt;package.json&lt;/code&gt; file, which tracks dependency relationships and other project settings.&lt;/p&gt;

&lt;p&gt;Now create a new file named &lt;code&gt;server.js&lt;/code&gt; in your project root. Begin by importing the necessary libraries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Import required modules&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Unblocker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unblocker&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We use &lt;code&gt;const&lt;/code&gt; declarations since these variables won't be reassigned. &lt;code&gt;require&lt;/code&gt; represents Node.js's module loading mechanism. When &lt;code&gt;require('module_name')&lt;/code&gt; executes, Node.js locates and loads the specified module, whether from core libraries or installed packages in &lt;code&gt;node_modules&lt;/code&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  Configuring the Proxy Server Instance
&lt;/h3&gt;

&lt;p&gt;Now configure the Express application and Node Unblocker middleware:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Initialize Express application&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// Set up Node Unblocker instance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unblocker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Unblocker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/myproxy/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// Custom prefix&lt;/span&gt;

&lt;span class="c1"&gt;// Register unblocker middleware&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unblocker&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, create an Express application instance (&lt;code&gt;app&lt;/code&gt;). Then initialize Node Unblocker (&lt;code&gt;unblocker&lt;/code&gt;) with a configuration object. The &lt;code&gt;prefix&lt;/code&gt; option (e.g., &lt;code&gt;/myproxy/&lt;/code&gt;) defines the URL path that activates the proxy. Accessing an address like &lt;code&gt;http://yourserver/myproxy/https://example.com&lt;/code&gt; routes the request through Node Unblocker. Requests lacking this prefix bypass the unblocker middleware entirely.&lt;/p&gt;

&lt;p&gt;Finally, &lt;code&gt;app.use(unblocker)&lt;/code&gt; registers the Node Unblocker instance as Express middleware. Consequently, all incoming requests matching the prefix get processed by unblocker.&lt;/p&gt;

&lt;p&gt;You may also specify a server listening port:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Define port number (optional, defaults typically suffice)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customPort&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8081&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Starting the Proxy Server
&lt;/h3&gt;

&lt;p&gt;With configuration complete, instruct the Express application to begin accepting connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Define server listening port&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;customPort&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;8080&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Prioritize environment variable, then custom port, finally default 8080&lt;/span&gt;

&lt;span class="c1"&gt;// Launch server and handle protocol upgrades&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upgrade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unblocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onUpgrade&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Output confirmation message&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Node Unblocker proxy server running on port: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;app.listen(PORT)&lt;/code&gt; line initiates the server. It attempts to use a port defined in environment variables (&lt;code&gt;process.env.PORT&lt;/code&gt;), falls back to our &lt;code&gt;customPort&lt;/code&gt; if undefined, and ultimately defaults to 8080. This flexible port handling proves valuable in cloud platform deployment environments.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.on('upgrade', unblocker.onUpgrade)&lt;/code&gt; segment is crucial. It enables Node Unblocker to handle protocol upgrade requests, such as those required by WebSockets. This guarantees compatibility with websites employing modern communication protocols beyond standard HTTP/HTTPS.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;console.log&lt;/code&gt; line simply outputs a confirmation message to the console, indicating the server is operational and its port number.&lt;/p&gt;




&lt;h3&gt;
  
  
  Local Testing and Verification
&lt;/h3&gt;

&lt;p&gt;Before deploying remotely, always test your Node Unblocker instance locally to identify any fundamental errors.&lt;/p&gt;

&lt;p&gt;Open your terminal and navigate to the project directory (if not already there):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/to/your/project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then launch the server using Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node server.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(Substitute &lt;code&gt;server.js&lt;/code&gt; with your actual filename if different.)&lt;/p&gt;

&lt;p&gt;Once the confirmation message appears in the console, open a web browser and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:PORT/myproxy/https://example.com/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ensure you replace &lt;code&gt;PORT&lt;/code&gt; with the actual port number displayed in your console (e.g., 8081 or 8080), &lt;code&gt;/myproxy/&lt;/code&gt; with your configured prefix, and &lt;code&gt;https://example.com/&lt;/code&gt; with the site you wish to test.&lt;/p&gt;

&lt;p&gt;When everything is properly configured, the target website should load in your browser, served through your local Node Unblocker proxy. You can also verify this using command-line tools like cURL.&lt;/p&gt;




&lt;h3&gt;
  
  
  Deploying to a Cloud Server
&lt;/h3&gt;

&lt;p&gt;Running Node Unblocker locally suits testing or bypassing simple network restrictions, but it fails to conceal your IP address for accessing geo-restricted content or conducting large-scale scraping operations.&lt;/p&gt;

&lt;p&gt;Deploying Node Unblocker to a cloud server provides it with a distinct IP address, enabling you to bypass geographical limitations, avoid IP blocks, and execute more demanding web scraping tasks.&lt;/p&gt;

&lt;p&gt;Numerous cloud providers offer virtual machines suitable for this purpose, including Google Cloud Platform (GCP), AWS EC2, DigitalOcean Droplets, Heroku, or Render. This guide uses Google Cloud Compute Engine as an example due to its accessible low-cost options.&lt;/p&gt;

&lt;p&gt;First, ensure your &lt;code&gt;package.json&lt;/code&gt; file is deployment-ready. You may need to specify the Node.js version and a start script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-node-unblocker"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A simple Node Unblocker proxy server"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"private"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"engines"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;=18.0.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"express"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^4.18.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"unblocker"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^2.3.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;scripts.start&lt;/code&gt; command informs the hosting platform how to execute your application. The &lt;code&gt;engines.node&lt;/code&gt; field specifies the required Node.js version range for compatibility.&lt;/p&gt;

&lt;p&gt;Next, register an account with your chosen cloud provider (e.g., Google Cloud) and create a new VM instance. Typically, you'll select an operating system (such as Ubuntu or Debian), a machine type (smaller, less expensive options often suffice), and a region.&lt;/p&gt;

&lt;p&gt;Choose an appropriate instance specification (for example, &lt;code&gt;e2-micro&lt;/code&gt; or &lt;code&gt;e2-small&lt;/code&gt; on GCP are generally cost-effective) and launch it.&lt;/p&gt;

&lt;p&gt;Once the instance is running, connect to it. Most cloud providers offer browser-based SSH access, or you can use a standard SSH client from your local terminal.&lt;/p&gt;

&lt;p&gt;After connecting, you'll typically enter a Linux shell environment (like Ubuntu). You may need to re-authenticate your cloud account within the SSH session.&lt;/p&gt;

&lt;p&gt;When using Ubuntu/Debian, you might need to modify the &lt;code&gt;app.listen&lt;/code&gt; call in your &lt;code&gt;server.js&lt;/code&gt; to bind to all network interfaces, permitting external connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Listen on all interfaces for external access&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.0.0.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;upgrade&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unblocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onUpgrade&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Node Unblocker proxy server running on port: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, externally accessible`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now upload your project files (&lt;code&gt;server.js&lt;/code&gt;, &lt;code&gt;package.json&lt;/code&gt;) to the VM. Browser-based SSH usually includes an "Upload Files" button. Alternatively, use &lt;code&gt;scp&lt;/code&gt; from your local machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;scp server.js package.json username@VM_EXTERNAL_IP:/path/on/server/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With files uploaded, install Node.js and npm on the VM. Consult the NodeSource documentation for current installation instructions suited to your Linux distribution. This typically involves adding their repository and then using &lt;code&gt;apt-get&lt;/code&gt; or &lt;code&gt;yum&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After installing Node.js, navigate to your project directory on the VM, install dependencies, and start the server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; /path/on/server/
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Upon success, you should see the confirmation message. Now, from your &lt;em&gt;local&lt;/em&gt; machine's browser, attempt to access a site through the deployed proxy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://VM_EXTERNAL_IP:PORT/myproxy/https://httpbin.org/ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replace &lt;code&gt;VM_EXTERNAL_IP&lt;/code&gt; with your VM's public IP address, &lt;code&gt;PORT&lt;/code&gt; with the correct port number, and &lt;code&gt;/myproxy/&lt;/code&gt; with your prefix. Using a site like &lt;code&gt;httpbin.org/ip&lt;/code&gt; displays the IP address making the request—it should show your VM's IP, not your local one.&lt;/p&gt;

&lt;p&gt;If you encounter connection errors, you may need to configure firewall rules in your cloud provider's settings to permit incoming traffic on the specific port your Node Unblocker server uses (e.g., TCP port 8080 or 8081).&lt;/p&gt;




&lt;h3&gt;
  
  
  Limitations and Future Considerations
&lt;/h3&gt;

&lt;p&gt;You now possess a fully operational Node Unblocker proxy server, deployable either locally or on a remote machine. Provided it complies with your cloud provider's terms of service, it can serve as a valuable asset for circumventing certain web restrictions or for small-scale web scraping projects.&lt;/p&gt;

&lt;p&gt;However, relying on a single Node Unblocker instance on one VM means all your requests originate from a single IP address. For any substantial scraping activity, this IP can rapidly become flagged and blocked by target websites.&lt;/p&gt;

&lt;p&gt;Node Unblocker configurations work best for light usage scenarios, or when you possess the resources and technical expertise to manage multiple instances across different VMs—essentially constructing a small, self-managed proxy network. This approach helps distribute requests and diminishes the probability of blocks.&lt;/p&gt;

&lt;p&gt;For larger, more demanding projects, or when managing numerous VMs becomes burdensome and expensive, transitioning to a dedicated proxy service provider often proves more practical. Professional services offer extensive pools of diverse IP addresses (Residential, Mobile, Datacenter) engineered for scale, typically delivering superior reliability, broader geographic coverage, and frequently a lower cost per IP or per GB compared to operating multiple individual VMs.&lt;/p&gt;




&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;Node Unblocker provides developers with a low-cost, highly flexible proxy solution. Following the steps outlined in this guide, you can rapidly establish your own proxy server, whether for testing purposes or small-scale data collection projects.&lt;/p&gt;

&lt;p&gt;Key takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Node Unblocker builds on the Express framework with straightforward configuration&lt;/li&gt;
&lt;li&gt;Middleware extensions support customized request processing logic&lt;/li&gt;
&lt;li&gt;Local deployment facilitates testing; cloud deployment achieves IP isolation&lt;/li&gt;
&lt;li&gt;Single instances carry IP exposure risks; larger projects benefit from professional proxy services&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Best Residential Proxies 2026: Avoid Kimwolf Botnet and Criminal Risks</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Wed, 28 Jan 2026 01:44:34 +0000</pubDate>
      <link>https://dev.to/miller_proxy/best-residential-proxies-2026-avoid-kimwolf-botnet-and-criminal-risks-31dc</link>
      <guid>https://dev.to/miller_proxy/best-residential-proxies-2026-avoid-kimwolf-botnet-and-criminal-risks-31dc</guid>
      <description>&lt;p&gt;The residential proxy market took a dark turn in late 2025 when the Kimwolf botnet emerged, compromising over 2 million Android devices and transforming innocent household internet connections into criminal infrastructure. If you're evaluating proxy providers right now, this isn't just about speed or IP pool size anymore—it's about whether your provider might inadvertently connect you to a criminal network.&lt;/p&gt;

&lt;p&gt;I've spent the past three months investigating how major proxy services source their IPs following the FBI's June 2025 advisory on BADBOX 2.0 and the subsequent Kimwolf revelations. What I found reshaped how we evaluate providers at proxy001.com, and it should change how you approach this decision too.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Kimwolf Reality: What Actually Happened
&lt;/h2&gt;

&lt;p&gt;In October 2025, security researcher Benjamin Brundage at Synthient began tracking an Android botnet that would eventually infect millions of devices. The infection vector wasn't sophisticated malware—it was residential proxy software already installed on cheap Android TV boxes and digital photo frames sold through major e-commerce platforms.&lt;/p&gt;

&lt;p&gt;According to Synthient's January 2026 research, the botnet exploited a fundamental weakness: approximately 67% of devices in residential proxy pools had unauthenticated Android Debug Bridge (ADB) services. Attackers manipulated DNS records to bypass RFC 1918 private IP range restrictions, tunneling into home networks through legitimate proxy endpoints. Once inside, they could scan and infect vulnerable devices within minutes.&lt;/p&gt;

&lt;p&gt;The geographic distribution tells its own story—heavy concentrations in Vietnam, Brazil, India, and Saudi Arabia, with approximately 12 million unique IP addresses observed weekly. Infoblox's analysis found that nearly 25% of their enterprise customers had made queries to Kimwolf-related domains since October 2025, indicating the botnet had penetrated corporate and government networks through employee devices running proxy software.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Criminal Liability Problem You Haven't Considered
&lt;/h2&gt;

&lt;p&gt;Here's the aspect most "best proxy" articles won't discuss: when you route traffic through a botnet-compromised residential IP, you may be participating in criminal infrastructure without knowing it.&lt;/p&gt;

&lt;p&gt;The FBI's 911 S5 takedown in May 2024 established clear precedent. That botnet operated since 2014, compromising 19 million IP addresses across 190 countries. The DOJ estimated $5.9 billion in fraud losses from 560,000 fraudulent unemployment claims alone, plus over 47,000 fraudulent Economic Injury Disaster Loan applications—all routed through what appeared to be ordinary residential connections.&lt;/p&gt;

&lt;p&gt;The administrator, YunHe Wang, now faces 65 years in prison. But the legal exposure extended beyond operators. Businesses that used these proxy services for data collection, ad verification, or market research suddenly found their activities intermingled with criminal infrastructure. Treasury sanctions followed, freezing assets and complicating legitimate business operations.&lt;/p&gt;

&lt;p&gt;The pattern repeated with Cloud Router (911 S5's successor), BADBOX, and now Kimwolf. Each time, the question became harder to answer: how do you prove your traffic wasn't part of the problem?&lt;/p&gt;

&lt;h2&gt;
  
  
  Due Diligence Framework: Questions Most Buyers Forget to Ask
&lt;/h2&gt;

&lt;p&gt;After analyzing incident reports and speaking with compliance teams at affected organizations, I've developed a verification framework that goes beyond marketing claims.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP Sourcing Transparency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Legitimate providers can explain exactly how they acquire residential IPs. The ethical standard—established partly through the EWDCI (Ethical Web Data Collection Initiative)—requires explicit user consent and fair compensation. Ask providers directly: "How do you acquire your residential IPs?" Vague answers about "partnerships" or "proprietary networks" are red flags.&lt;/p&gt;

&lt;p&gt;Reputable providers like those with EWDCI Certified designation (currently including Oxylabs, Rayobyte, Smartproxy, NetNut, and Zyte) have undergone third-party verification of their sourcing practices. This doesn't guarantee immunity from botnet contamination, but it establishes a baseline of accountability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device Verification Practices&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;After Kimwolf, this became critical. Providers should actively scan their networks for signs of botnet activity and remove suspicious endpoints. Ask whether they monitor for: exposed ADB services, anomalous traffic patterns, pre-infected device signatures, and connections to known C2 infrastructure.&lt;/p&gt;

&lt;p&gt;The honest answer is that this monitoring is imperfect. Synthient's research showed Kimwolf infections could establish themselves within minutes of a device joining a proxy pool. But providers who admit these limitations while explaining their mitigation strategies are more trustworthy than those claiming perfect security.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance Documentation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Request evidence of GDPR and CCPA compliance, KYC (Know Your Customer) procedures for clients, and acceptable use policies with actual enforcement mechanisms. Providers serving enterprise clients typically maintain SOC 2 certification or equivalent third-party security audits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Indicators of Potentially Compromised Networks
&lt;/h2&gt;

&lt;p&gt;When testing residential proxy services, watch for these warning signs that emerged from Kimwolf analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unusual Geographic Concentrations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kimwolf's heavy presence in Vietnam, Brazil, India, and Saudi Arabia wasn't random—these markets have high concentrations of cheap Android TV boxes with poor security. If a provider's pool shows disproportionate availability from these regions without transparent explanation, investigate further.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing Anomalies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Kimwolf operators sold residential proxy access for as low as $0.20 per GB—far below sustainable rates for ethically-sourced IPs. Legitimate residential proxies require compensating device owners, maintaining infrastructure, and implementing security measures. Extremely low pricing often indicates compromised sourcing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Response Time Inconsistencies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Botnet-based proxies often show unusual latency patterns because traffic routes through consumer devices with variable connection quality. While residential proxies naturally have more variable performance than datacenter alternatives, extreme inconsistency may indicate infected device pools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ISP Proxy Alternative Worth Considering
&lt;/h2&gt;

&lt;p&gt;The Kimwolf situation accelerated interest in ISP proxies—IPs sourced directly from internet service providers rather than consumer devices. While ISP proxies have existed for years, they've become increasingly attractive for businesses prioritizing security over pure residential authenticity.&lt;/p&gt;

&lt;p&gt;The tradeoff is real. ISP proxies offer greater stability and lower botnet risk because they don't depend on consumer device networks. However, sophisticated anti-bot systems can distinguish ISP proxy traffic from genuine residential connections. For some use cases—particularly those requiring the absolute highest trust scores—residential proxies remain necessary.&lt;/p&gt;

&lt;p&gt;The practical approach for most organizations is a hybrid strategy: ISP proxies for routine operations, verified residential proxies for specific requirements, and continuous monitoring regardless of proxy type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building a Verification Process That Actually Works
&lt;/h2&gt;

&lt;p&gt;Based on our experience evaluating providers post-Kimwolf, here's a practical verification sequence.&lt;/p&gt;

&lt;p&gt;Start with public information. Check whether the provider has EWDCI certification, SOC 2 reports, or other third-party validations. Review their published privacy policy and acceptable use terms for specificity—vague policies suggest unclear practices.&lt;/p&gt;

&lt;p&gt;Request direct answers about IP sourcing. A provider comfortable explaining their acquisition methods, compensation structures for device owners, and consent mechanisms has nothing to hide. Reluctance or deflection suggests potential problems.&lt;/p&gt;

&lt;p&gt;Test before committing significant resources. Most reputable providers offer trial access. During trials, monitor for the technical indicators described above. Use services like Spur.us to check whether test IPs appear on known botnet or abuse lists.&lt;/p&gt;

&lt;p&gt;Establish ongoing monitoring. The proxy landscape shifts constantly. Providers that appear clean today may face contamination tomorrow as botnets evolve. Maintain relationships with security-focused customers and industry researchers who track these developments.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Got Wrong Initially
&lt;/h2&gt;

&lt;p&gt;I'll be honest about our own learning curve. When Kimwolf first emerged, we assumed that simply avoiding the cheapest providers would protect against botnet exposure. That assumption proved naive.&lt;/p&gt;

&lt;p&gt;Synthient's research showed that IPIDEA—described as the world's leading IP proxy provider with 6.1 million daily updated addresses—had significant Kimwolf contamination despite being a major market player. The issue wasn't provider size or pricing tier but rather fundamental security architecture that allowed DNS manipulation to bypass network restrictions.&lt;/p&gt;

&lt;p&gt;This reinforced an uncomfortable truth: no provider can guarantee complete protection from increasingly sophisticated botnet operators. The best you can achieve is working with providers who acknowledge this reality, invest seriously in detection and mitigation, and maintain transparency when incidents occur.&lt;/p&gt;




&lt;h2&gt;
  
  
  Secure Your Operations with Proxy001's Verified Residential Network
&lt;/h2&gt;

&lt;p&gt;At proxy001.com, we've built our residential proxy infrastructure with post-Kimwolf security requirements as foundational design principles—not afterthoughts. Our network sources IPs exclusively through transparent consent-based partnerships where device owners receive fair compensation and maintain complete control over their participation.&lt;/p&gt;

&lt;p&gt;Every IP in our pool undergoes continuous monitoring for botnet signatures, exposed services, and anomalous behavior patterns. We maintain documented KYC procedures for all clients and provide detailed compliance documentation for enterprise security reviews. Our technical team actively tracks emerging threats through relationships with security researchers and promptly removes any endpoints showing compromise indicators.&lt;/p&gt;

&lt;p&gt;Try our residential proxy network with confidence—start with our trial access to verify performance and security characteristics in your specific use case. Visit proxy001.com to learn how we protect your operations from the criminal risks plaguing less vigilant providers.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Last updated: January 2026. This article reflects security conditions as of publication date. The residential proxy landscape continues evolving, and we recommend ongoing monitoring of security advisories from FBI, CISA, and independent researchers.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Static vs Rotating Proxies 2026: Which Is Better for Your Needs?</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Tue, 27 Jan 2026 05:39:38 +0000</pubDate>
      <link>https://dev.to/miller_proxy/static-vs-rotating-proxies-2026-which-is-better-for-your-needs-3a3c</link>
      <guid>https://dev.to/miller_proxy/static-vs-rotating-proxies-2026-which-is-better-for-your-needs-3a3c</guid>
      <description>&lt;h2&gt;
  
  
  The Decision That Determines Your Project's Success Rate
&lt;/h2&gt;

&lt;p&gt;Choosing between static and rotating proxies isn't a simple technical preference—it's a strategic decision that directly impacts whether your automation succeeds or fails. After configuring proxy setups for thousands of use cases over the past five years, we've learned that the "better" option depends entirely on what you're trying to accomplish.&lt;/p&gt;

&lt;p&gt;Here's what we've discovered: most users don't fail because they chose the wrong proxy type. They fail because they didn't understand &lt;em&gt;when&lt;/em&gt; each type excels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Core Difference
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Static proxies&lt;/strong&gt; assign you a single, unchanging IP address that remains yours for the duration of your session or subscription. Think of it as renting a specific apartment—the address stays the same every time you come home.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rotating proxies&lt;/strong&gt; cycle through multiple IP addresses, either per request, at set time intervals, or based on session rules. Each connection can appear to originate from a different location, making your traffic pattern look like multiple distinct users.&lt;/p&gt;

&lt;p&gt;This fundamental difference creates distinct advantages and limitations for specific tasks.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Static Proxies Are the Right Choice
&lt;/h2&gt;

&lt;p&gt;Static residential proxies shine in scenarios requiring identity consistency. Based on our operational data, here's where they perform best:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Social Media Account Management&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Platform algorithms flag accounts that show erratic location patterns. When you log into Instagram from Chicago at 9 AM, then suddenly appear in London at 9:05 AM, the platform notices. Static proxies eliminate this problem by maintaining geographic and IP consistency across sessions.&lt;/p&gt;

&lt;p&gt;For agencies managing 20+ client accounts, we typically recommend assigning one static residential IP per account. This approach has reduced account suspension rates significantly in our client deployments compared to shared or rotating setups.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;E-commerce Operations&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sneaker copping, limited-release purchases, and marketplace account management all require session persistence. Checkout flows that span multiple pages can break when your IP changes mid-transaction—payment processors and inventory systems track session continuity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitelisted Access Systems&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some APIs and platforms authenticate based on IP address. Financial data providers, enterprise SaaS tools, and B2B platforms often require static IPs for their whitelist configurations. Rotating IPs would trigger re-authentication on every request.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Honest Drawback&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Static proxies have a fundamental limitation: they're identifiable. If you send 10,000 requests from the same IP to a single target, sophisticated anti-bot systems will eventually notice. They're also more expensive per IP and don't scale well for large-volume data collection.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Rotating Proxies Become Essential
&lt;/h2&gt;

&lt;p&gt;Rotating residential proxies were designed to solve a specific problem: making high-volume automated requests appear as organic traffic from multiple users. Here's where rotation is not just helpful but necessary:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Large-Scale Web Scraping&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Modern websites implement request rate limits per IP address. Exceed 20-50 requests from the same IP on a site like Amazon, and you'll trigger throttling, CAPTCHAs, or outright blocks. Rotating proxies distribute your requests across thousands of IPs, keeping each address under detection thresholds.&lt;/p&gt;

&lt;p&gt;One travel industry client we worked with needed to monitor pricing across 18 regional booking platforms. Their previous static proxy setup hit blocks within hours. After switching to a rotating pool with per-domain concurrency limits, they achieved consistent daily coverage with under 3% failure rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SERP Monitoring and SEO Research&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search engines aggressively rate-limit repeated queries. Tracking 500 keywords across multiple geographic regions requires IP diversity—attempting this with static proxies would result in CAPTCHA challenges before you finish the first batch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Competitive Intelligence&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Monitoring competitor pricing, inventory levels, and content changes requires frequent checks across multiple targets. Rotation allows you to maintain ongoing surveillance without burning through IP reputation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ad Verification&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Verifying that ads display correctly across regions and detecting fraudulent placements requires accessing sites from diverse IP pools without revealing you're running verification checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Trade-offs to Consider&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rotating proxies aren't perfect. Session persistence can be tricky—if you need to maintain a logged-in state across multiple requests, you'll need to configure sticky sessions properly. Some providers support session durations from 1 minute to 24 hours, but this adds complexity.&lt;/p&gt;

&lt;p&gt;There's also slight latency overhead during rotation. For time-sensitive operations, the milliseconds lost during IP switching can matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Comparison: Performance Characteristics
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Factor&lt;/th&gt;
&lt;th&gt;Static Proxies&lt;/th&gt;
&lt;th&gt;Rotating Proxies&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Session Stability&lt;/td&gt;
&lt;td&gt;Excellent—same IP indefinitely&lt;/td&gt;
&lt;td&gt;Variable—requires sticky session config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detection Risk&lt;/td&gt;
&lt;td&gt;Higher if overused on single target&lt;/td&gt;
&lt;td&gt;Lower with proper rotation frequency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Generally faster (no rotation overhead)&lt;/td&gt;
&lt;td&gt;Slight latency during switches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scalability&lt;/td&gt;
&lt;td&gt;Limited by IP count purchased&lt;/td&gt;
&lt;td&gt;Excellent—access to large pools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost Model&lt;/td&gt;
&lt;td&gt;Per IP (often monthly)&lt;/td&gt;
&lt;td&gt;Per GB of bandwidth&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best For&lt;/td&gt;
&lt;td&gt;Account management, whitelisted access&lt;/td&gt;
&lt;td&gt;Scraping, monitoring, research&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Rotation Frequency: Finding the Right Balance
&lt;/h2&gt;

&lt;p&gt;One detail that trips up many users: rotation frequency matters more than simply "rotating vs. not rotating."&lt;/p&gt;

&lt;p&gt;Rotating after every single request maximizes anonymity but can cause issues with sites that expect some session continuity. Rotating every 10-20 requests often provides better success rates on most targets while still avoiding detection patterns.&lt;/p&gt;

&lt;p&gt;The optimal frequency depends on your target. Some sites tolerate extended sessions from single IPs; others flag repeated access almost immediately. We recommend starting with per-request rotation for new scraping projects, then testing longer intervals to find the threshold that works for your specific targets.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hybrid Approach: Using Both
&lt;/h2&gt;

&lt;p&gt;Experienced operators rarely choose exclusively. A sophisticated setup might use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Static residential IPs&lt;/strong&gt; for managing social media accounts and maintaining persistent sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rotating residential IPs&lt;/strong&gt; for data collection and research tasks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sticky sessions&lt;/strong&gt; (rotating proxies held for defined periods) for checkout flows and multi-step processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This combination covers most operational needs while optimizing costs—static IPs for tasks requiring them, rotating bandwidth for volume operations.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common Implementation Mistakes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Mistake 1: Using static proxies for scraping at scale&lt;/strong&gt;&lt;br&gt;
We see this constantly. Users buy 10 static IPs thinking they can scrape large datasets, then burn through those IPs within days because they lack the volume for meaningful distribution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 2: Ignoring sticky session options&lt;/strong&gt;&lt;br&gt;
Rotating proxies with sticky session support (maintaining the same IP for 5-30 minutes) can handle many account-based tasks that seem to require static IPs. Test this before committing to static-only infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mistake 3: Rotating too aggressively&lt;/strong&gt;&lt;br&gt;
Changing IPs after every request looks suspicious to sophisticated anti-bot systems. Sometimes appearing as a normal user making several requests is less detectable than appearing as dozens of users making one request each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Your Decision: A Practical Framework
&lt;/h2&gt;

&lt;p&gt;Ask yourself these questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does my task require maintaining the same identity over time?&lt;/strong&gt; (Account management, whitelisted access) → Static&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Am I making high volumes of requests to the same targets?&lt;/strong&gt; (Scraping, monitoring) → Rotating&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do I need both session persistence AND volume capacity?&lt;/strong&gt; → Hybrid approach with sticky sessions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's my primary constraint—cost per IP or cost per GB?&lt;/strong&gt; → This determines which pricing model makes sense&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting Started with the Right Proxy Infrastructure
&lt;/h2&gt;

&lt;p&gt;Whether you need static residential proxies for account management or rotating residential IPs for large-scale data collection, proxy001.com provides both options with genuine residential IPs from real ISP connections. Our network supports flexible session configurations—from fully static assignments to per-request rotation and everything in between. &lt;/p&gt;

&lt;p&gt;New users can test our residential proxy infrastructure to see which configuration works best for their specific use case. Our technical support team can help you determine the optimal setup based on your targets and volume requirements. Visit &lt;a href="https://proxy001.com" rel="noopener noreferrer"&gt;proxy001.com&lt;/a&gt; to explore our residential proxy solutions and start with a configuration matched to your actual needs.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Rotating Residential Proxies Still Get Blocked: A Diagnostic Framework to Separate Site Policy vs Proxy Quality Signals</title>
      <dc:creator>Miller James</dc:creator>
      <pubDate>Mon, 12 Jan 2026 02:28:16 +0000</pubDate>
      <link>https://dev.to/miller_proxy/rotating-residential-proxies-still-get-blocked-a-diagnostic-framework-to-separate-site-policy-vs-3mcj</link>
      <guid>https://dev.to/miller_proxy/rotating-residential-proxies-still-get-blocked-a-diagnostic-framework-to-separate-site-policy-vs-3mcj</guid>
      <description>&lt;p&gt;When rotating residential proxies fail to prevent blocks, the immediate instinct is to switch providers or increase rotation frequency. This approach wastes budget and troubleshooting time because it assumes all failures stem from IP quality—when many originate from site policies, behavioral detection, or configuration errors that no amount of rotation will solve.&lt;/p&gt;

&lt;p&gt;This diagnostic framework provides the attribution logic, measurement definitions, and stop conditions you need to determine whether your blocking issues indicate a proxy quality limitation you can fix, or a policy boundary you must accept.&lt;/p&gt;

&lt;h2&gt;
  
  
  Direct Diagnosis: When Rotating Residential Proxies Won't Help—and What to Check First
&lt;/h2&gt;

&lt;p&gt;Before investing in a new residential rotating proxy provider or tweaking rotation settings, you need to attribute the failure correctly. Bot management systems use multiple detection signals including behavioral analysis, machine learning, and fingerprinting to classify traffic as human or automated. IP reputation is one signal among many; behavioral signals and device fingerprints provide additional classification layers that persist regardless of your IP rotation strategy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Direct Answer Block (TEMPLATE)
&lt;/h3&gt;

&lt;p&gt;The following two-bucket framework separates observable signals into their likely attribution categories. Use this before making any changes to your proxy for web scraping setup.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Policy/Blocking-Signal Indicators vs Proxy-Quality/Configuration Indicators&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Policy/Blocking-Signal Indicators&lt;/th&gt;
&lt;th&gt;Proxy-Quality/Configuration Indicators&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Observable Signals&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;HTTP 403 with challenge page content; CAPTCHA challenges appearing consistently across different IPs; identical block patterns regardless of proxy rotation; behavioral challenge triggers (mouse movement, timing verification); TLS fingerprint mismatch errors&lt;/td&gt;
&lt;td&gt;HTTP 407 Proxy Authentication Required; connection timeouts concentrated on specific proxy endpoints; inconsistent success rates across proxy pool segments; HTTP 503 with no challenge content; connection reset by peer errors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Evidence to Collect&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Response body content (challenge page vs error page); timing between request and block; whether block persists after IP change; presence of JavaScript challenge requirements; cookie persistence behavior&lt;/td&gt;
&lt;td&gt;Proxy endpoint response times; authentication header verification; proxy provider status page; success rate differential between proxy pool segments; error distribution by proxy endpoint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Acceptance Condition&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Block pattern identical across 5+ distinct IPs with varied timing&lt;/td&gt;
&lt;td&gt;Success rate improves when switching proxy endpoints or fixing configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Stop Action / Remediation Path&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Stop proxy rotation attempts; assess compliance requirements; consider whether data need justifies alternative approaches&lt;/td&gt;
&lt;td&gt;Check proxy credentials; test with different proxy pool; review session and rotation configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;When Uncertain:&lt;/strong&gt; If evidence is ambiguous, collect diagnostic evidence pack (see Measurement section) before making changes. Log at minimum: timestamp, request_id, target_url, proxy_ip_hash, http_status_code, response_time_ms, content_hash, error_type.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation Path:&lt;/strong&gt; For policy-type blocks, consult legal counsel regarding ToS/CFAA compliance. For quality-type issues, contact proxy vendor support with diagnostic evidence pack.&lt;/p&gt;

&lt;p&gt;Evidence: FILE_02_assets_blueprints.json#direct_answer_block, FILE_01_knowledge_base.jsonl#KB001, KB002, KB004&lt;/p&gt;




&lt;h2&gt;
  
  
  Define Your Test Unit Before You Troubleshoot: Request vs Session vs Workflow Boundary
&lt;/h2&gt;

&lt;p&gt;Troubleshooting failures requires understanding what constitutes a "success" for your specific use case. A single HTTP 200 response may be meaningless if you need to complete a multi-step authentication flow, and a high success rate on product listing pages tells you nothing about checkout flow reliability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Request-Level Unit:&lt;/strong&gt; Single HTTP request/response cycle. Appropriate for stateless data collection where each request is independent. Use rotating residential proxies with per-request rotation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session-Level Unit:&lt;/strong&gt; Sequence of requests sharing session state (cookies, authentication tokens). Session-based workflows commonly fail when rotation occurs mid-flow. Use sticky sessions or session-persistent proxy configuration. Session ID persistence is required for multi-step workflows; lost session causes authentication failure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Workflow-Level Unit:&lt;/strong&gt; Complete business process (login → navigate → action → confirm). Requires maintaining identity consistency across the entire flow. A single failed step invalidates the entire workflow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Decision Cue:&lt;/strong&gt; If your workflow involves authentication, shopping carts, or any state that must persist between requests, per-request rotation will likely break the flow. The decision between sticky session vs rotating proxy depends on workflow type, but specific decision criteria with quantified thresholds are not provided in the available documentation—you must test against your specific target.&lt;/p&gt;




&lt;h2&gt;
  
  
  Read the Block, Don't Guess: A Symptom Taxonomy That Tells You "Policy Signal" vs "Quality Limit" Before Switching Proxies
&lt;/h2&gt;

&lt;p&gt;This section provides the Gap Slot for G01: the core attribution logic that prevents blind proxy switching. Each symptom category includes evidence requirements and a validation checkpoint.&lt;/p&gt;

&lt;h3&gt;
  
  
  Symptom Categories and Attribution
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;HTTP 4xx Responses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;403 Forbidden:&lt;/strong&gt; Server understood request but refuses to authorize it, distinct from authentication failure. This may indicate either policy-based blocking OR IP reputation issues. Evidence to collect: response body content (look for challenge page HTML vs generic error), whether the same 403 appears from different IPs, timing patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;407 Proxy Authentication Required:&lt;/strong&gt; Indicates proxy-level auth failure, separate from target server. This is a configuration issue, not a site policy block. Evidence to collect: verify proxy credentials, check proxy endpoint status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;429 Too Many Requests:&lt;/strong&gt; Indicates rate limiting; client should reduce request frequency. This is typically rate limiting, not IP blocking—rotation cannot fix capacity issues. Evidence to collect: Retry-After header value, request frequency logs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;HTTP 5xx Responses&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;503 Service Unavailable:&lt;/strong&gt; May indicate server overload or maintenance, potentially temporary. Could be legitimate server issue OR soft blocking. Evidence to collect: whether 503 is consistent across time, response body content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;CAPTCHA/Challenge Triggers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;CAPTCHA challenges are triggered when traffic exhibits suspicious patterns but confidence is insufficient for outright blocking. A high CAPTCHA rate suggests detection threshold proximity. If CAPTCHAs appear consistently across different IPs, this indicates behavioral or fingerprint detection—not IP reputation alone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Connection Failures&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Connection reset by peer:&lt;/strong&gt; May indicate proxy or intermediate network issue—not necessarily target site blocking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSL/TLS handshake failures:&lt;/strong&gt; Suggest certificate or protocol mismatch. This can indicate TLS fingerprint detection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout errors:&lt;/strong&gt; May indicate network/proxy issues rather than target site blocking.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Session/Auth Failures&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Session loss after IP rotation indicates workflow incompatibility with per-request rotation, not a blocking issue. Login/checkout flows commonly fail on mid-flow rotation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Content Anomalies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Empty responses, truncated content, or served content that differs from expected may indicate soft blocking. Evidence to collect: content_hash comparison between requests, response body length patterns.&lt;/p&gt;

&lt;h3&gt;
  
  
  Text-Based Flowchart: Symptom Triage Path (TEMPLATE)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;START: Observe Failure Symptom
    |
    v
CLASSIFY SYMPTOM TYPE
    |
    +--[HTTP 4xx]---------&amp;gt; CHECK 4XX SUBTYPE
    |                           |
    |                           +--[403 Forbidden]----&amp;gt; CHECK FOR NON-IP SIGNALS
    |                           |                           |
    |                           |                           +--[Fingerprint/behavior signals present]
    |                           |                           |       |
    |                           |                           |       v
    |                           |                           |   RISK BOUNDARY (STOP)
    |                           |                           |   "Policy/Detection Boundary"
    |                           |                           |   Actions: Stop rotation attempts,
    |                           |                           |   assess compliance, accept limitation
    |                           |                           |
    |                           |                           +--[IP-only block likely]
    |                           |                                   |
    |                           |                                   v
    |                           |                               PROXY QUALITY ISSUE
    |                           |                               Actions: Check proxy success rate,
    |                           |                               test different pool, review config
    |                           |
    |                           +--[407 Proxy Auth]---&amp;gt; PROXY CONFIGURATION ISSUE
    |                           |                       Actions: Verify credentials, check endpoint
    |                           |
    |                           +--[429 Rate Limit]---&amp;gt; RATE LIMITING (not IP block)
    |                                                   Actions: Reduce rate, implement backoff
    |
    +--[HTTP 5xx]---------&amp;gt; CHECK 5XX PATTERN
    |                           |
    |                           +--[Consistent across time/IPs]--&amp;gt; Possible soft block
    |                           +--[Intermittent]--&amp;gt; Likely server issue, not blocking
    |
    +--[CAPTCHA/Challenge]--&amp;gt; CHECK CAPTCHA PATTERN
    |                           |
    |                           +--[Persists across IPs]--&amp;gt; RISK BOUNDARY (fingerprint/behavioral)
    |                           +--[Resolves with IP change]--&amp;gt; Proxy quality issue
    |
    +--[Connection Failure]--&amp;gt; CHECK CONNECTION TYPE
    |                           |
    |                           +--[TLS handshake fail]--&amp;gt; Possible fingerprint detection
    |                           +--[Timeout/reset]--&amp;gt; Likely proxy/network issue
    |
    +--[Session/Auth Fail]--&amp;gt; CHECK ROTATION MODE
    |                           |
    |                           +--[Using per-request rotation]--&amp;gt; Session consistency issue
    |                           +--[Using sticky session]--&amp;gt; Collect more evidence
    |
    +--[Content Anomaly]----&amp;gt; COMPARE CONTENT PATTERNS
                                |
                                +--[Consistent different content]--&amp;gt; Soft blocking detected
                                +--[Intermittent]--&amp;gt; Collect more evidence
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Validation Checkpoint:&lt;/strong&gt; Before switching proxies or providers, confirm you have collected evidence for at least 3 of these fields: http_status_code distribution, response body content samples, success rate by proxy segment, timing patterns, whether failures persist across 5+ distinct IPs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Measure What Matters Per Target: Success Rate, Block Rate, CAPTCHA Rate, Retry Rate (and Why "Headline Success Rate" Is Not Diagnostic)
&lt;/h2&gt;

&lt;p&gt;Vendor headline success rates are not actionable because they aggregate across targets, obscuring per-site performance that determines your actual outcomes. You need per-target measurement with bucketing dimensions to diagnose issues and compare providers meaningfully.&lt;/p&gt;

&lt;h3&gt;
  
  
  Measurement Plan Template (TEMPLATE)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Core Metrics&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric Name&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Formula&lt;/th&gt;
&lt;th&gt;Bucketing Dimensions&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;success_rate&lt;/td&gt;
&lt;td&gt;Proportion of requests returning expected content&lt;/td&gt;
&lt;td&gt;[Successful responses] / [Total requests]&lt;/td&gt;
&lt;td&gt;target_site, request_path, geo_region, time_window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;block_rate&lt;/td&gt;
&lt;td&gt;Proportion of requests receiving block responses&lt;/td&gt;
&lt;td&gt;[Block responses (403, challenge pages)] / [Total requests]&lt;/td&gt;
&lt;td&gt;target_site, block_type, time_window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;captcha_rate&lt;/td&gt;
&lt;td&gt;Proportion of requests triggering CAPTCHA challenges&lt;/td&gt;
&lt;td&gt;[CAPTCHA responses] / [Total requests]&lt;/td&gt;
&lt;td&gt;target_site, time_window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;retry_rate&lt;/td&gt;
&lt;td&gt;Proportion of requests requiring retry&lt;/td&gt;
&lt;td&gt;[Retried requests] / [Total requests]&lt;/td&gt;
&lt;td&gt;target_site, failure_type&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;latency_p50_p95_p99&lt;/td&gt;
&lt;td&gt;Response time percentiles&lt;/td&gt;
&lt;td&gt;Calculated from response_time_ms distribution&lt;/td&gt;
&lt;td&gt;target_site, geo_region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cost_per_success_action&lt;/td&gt;
&lt;td&gt;True cost including failed requests&lt;/td&gt;
&lt;td&gt;[Total bandwidth cost] / [Successful actions]&lt;/td&gt;
&lt;td&gt;target_site, action_type&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Bot score ranges from 1-99, where 1 indicates likely bot and 99 indicates likely human. Site operators can configure threshold actions where scores below threshold trigger block/challenge. CAPTCHA rate serves as diagnostic signal: high CAPTCHA rate suggests detection threshold proximity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acceptance Thresholds (TEMPLATE—Not provided in RAG; requires business context)&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Threshold Name&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Measurement Window&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;minimum_success_rate&lt;/td&gt;
&lt;td&gt;[Placeholder—define based on business requirements]&lt;/td&gt;
&lt;td&gt;[Placeholder]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maximum_block_rate&lt;/td&gt;
&lt;td&gt;[Placeholder—define based on acceptable failure rate]&lt;/td&gt;
&lt;td&gt;[Placeholder]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;maximum_captcha_rate&lt;/td&gt;
&lt;td&gt;[Placeholder—define based on operational tolerance]&lt;/td&gt;
&lt;td&gt;[Placeholder]&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;cost_stop_loss&lt;/td&gt;
&lt;td&gt;[Placeholder—define based on ROI requirements]&lt;/td&gt;
&lt;td&gt;[Placeholder]&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Authoritative benchmark values for these thresholds are not provided in the available documentation. Define thresholds based on your specific business requirements and acceptable failure rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logging Requirements&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Minimum diagnostic evidence pack fields (derived from Scrapy stats collection and community patterns):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;timestamp_utc&lt;/li&gt;
&lt;li&gt;request_id&lt;/li&gt;
&lt;li&gt;target_url&lt;/li&gt;
&lt;li&gt;proxy_ip_hash (privacy-safe hash, not raw IP)&lt;/li&gt;
&lt;li&gt;http_status_code&lt;/li&gt;
&lt;li&gt;response_time_ms&lt;/li&gt;
&lt;li&gt;content_length&lt;/li&gt;
&lt;li&gt;content_hash (for detecting content anomalies)&lt;/li&gt;
&lt;li&gt;error_type&lt;/li&gt;
&lt;li&gt;retry_count&lt;/li&gt;
&lt;li&gt;session_id&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scrapy Stats Collection Example:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stats collection middleware tracks request/response counts, status codes, and timing for debugging. Key diagnostic stats include: downloader/request_count, downloader/response_count, downloader/response_status_count/200, downloader/response_status_count/403, downloader/response_status_count/429, downloader/exception_count, retry/count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AutoThrottle Configuration (from Scrapy documentation):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AutoThrottle adjusts request delay based on server response latency, reducing load when server is slow. AUTOTHROTTLE_TARGET_CONCURRENCY setting controls average parallel requests to each remote server. Response latency serves as feedback signal for throttling decisions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Scrapy AutoThrottle configuration
&lt;/span&gt;&lt;span class="n"&gt;AUTOTHROTTLE_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;AUTOTHROTTLE_START_DELAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="n"&gt;AUTOTHROTTLE_MAX_DELAY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="n"&gt;AUTOTHROTTLE_TARGET_CONCURRENCY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Rotation vs Session Consistency: When Per-Request Rotation Breaks Auth, Carts, and Multi-Step Flows
&lt;/h2&gt;

&lt;p&gt;Per-request rotation with a rotating residential proxy can break workflows that depend on session state. Understanding when rotation causes failures—rather than prevents blocks—is essential for correct configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure Modes at Diagnostic Level
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Authentication Flows:&lt;/strong&gt; Login workflows require session continuity. If your residential rotating proxy rotates IP mid-authentication, the target site may invalidate the session, requiring re-authentication. Observable symptom: successful login followed by immediate logout or "session expired" errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopping Cart and Checkout:&lt;/strong&gt; E-commerce sites often tie cart state to session identity. IP changes mid-checkout may trigger fraud detection or simply lose cart contents. Observable symptom: cart emptied or checkout fails after successful item addition.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-Step Data Collection:&lt;/strong&gt; Workflows requiring pagination, form submission sequences, or state-dependent navigation may fail when rotation occurs between steps. Observable symptom: "previous step required" errors, missing context, or redirects to flow start.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configuration Patterns (Conceptual—Verify with Provider)
&lt;/h3&gt;

&lt;p&gt;HttpProxyMiddleware handles proxy configuration through request.meta['proxy'] or environment variables. Session management syntax varies by provider.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Conceptual pattern—specific implementation varies by proxy provider
# For rotating (per-request):
&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_rotating_proxy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# New IP each request
&lt;/span&gt;
&lt;span class="c1"&gt;# For sticky (session-persistent):
&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session_abc123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;proxy&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://user-session-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;session_id&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:pass@proxy.example.com:8080&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Decision Cue:&lt;/strong&gt; If workflow involves login, checkout, or any multi-step process requiring state, test with sticky session configuration before assuming proxy quality issues. Specific decision criteria with quantified thresholds for choosing sticky vs rotating are not provided in the available documentation—test against your specific target and workflow.&lt;/p&gt;

&lt;p&gt;If you're evaluating whether to buy rotating residential proxies or a residential rotating proxy service, consider that many providers offer both rotation modes. A rotating residential proxies free trial or residential rotating proxy free trial can help you test which mode works for your specific workflow before committing to buy residential rotating proxies for production use.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Defensive Troubleshooting Matrix: Symptom → Likely Bucket → Evidence to Collect → Acceptance Gate → Stop Condition
&lt;/h2&gt;

&lt;p&gt;This matrix provides the complete diagnostic pathway from observed symptom to attribution and action. Use it to systematically diagnose failures before changing your web scraping proxies configuration or switching web scraping proxy providers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting Matrix (TEMPLATE)
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom Category&lt;/th&gt;
&lt;th&gt;Example Signals&lt;/th&gt;
&lt;th&gt;Likely Bucket&lt;/th&gt;
&lt;th&gt;Evidence to Collect&lt;/th&gt;
&lt;th&gt;Acceptance Gate&lt;/th&gt;
&lt;th&gt;Stop Condition&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 403 Forbidden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Challenge page in response body; consistent across IPs&lt;/td&gt;
&lt;td&gt;Policy OR Quality&lt;/td&gt;
&lt;td&gt;Response body content; persistence across 5+ IPs; timing patterns&lt;/td&gt;
&lt;td&gt;If block persists across IPs with varied timing: Policy. If resolves with IP change: Quality&lt;/td&gt;
&lt;td&gt;Policy: Stop rotation attempts. Quality: Test different pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 407 Proxy Auth&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Proxy-level authentication failure&lt;/td&gt;
&lt;td&gt;Configuration&lt;/td&gt;
&lt;td&gt;Proxy credentials; endpoint status; authentication headers&lt;/td&gt;
&lt;td&gt;Resolves after credential fix&lt;/td&gt;
&lt;td&gt;Fix configuration; if persists, contact provider&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 429 Too Many Requests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Rate limit response with Retry-After header&lt;/td&gt;
&lt;td&gt;Rate Limiting (not IP block)&lt;/td&gt;
&lt;td&gt;Request frequency logs; Retry-After header value; concurrent request count&lt;/td&gt;
&lt;td&gt;Rate reduction resolves issue&lt;/td&gt;
&lt;td&gt;Implement backoff; reduce AUTOTHROTTLE_TARGET_CONCURRENCY&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;HTTP 503 Service Unavailable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Server unavailable, possibly temporary&lt;/td&gt;
&lt;td&gt;Quality OR Server Issue&lt;/td&gt;
&lt;td&gt;Consistency over time; response body content; affects multiple IPs&lt;/td&gt;
&lt;td&gt;If consistent: possible soft block. If intermittent: server issue&lt;/td&gt;
&lt;td&gt;Collect evidence over longer time window before attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CAPTCHA/Challenge&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;JavaScript challenge; image CAPTCHA; invisible CAPTCHA analysis&lt;/td&gt;
&lt;td&gt;Policy (if persistent)&lt;/td&gt;
&lt;td&gt;Challenge type; persistence across IPs; behavioral signals presence&lt;/td&gt;
&lt;td&gt;Persists across 5+ IPs: Policy boundary&lt;/td&gt;
&lt;td&gt;Stop rotation; assess non-IP detection signals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection Timeout&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;No response within timeout period&lt;/td&gt;
&lt;td&gt;Network OR Quality&lt;/td&gt;
&lt;td&gt;Timeout distribution by proxy endpoint; proxy provider status&lt;/td&gt;
&lt;td&gt;Concentrated on specific endpoints: proxy issue&lt;/td&gt;
&lt;td&gt;Test alternate endpoints; contact provider if persists&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connection Reset&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Connection reset by peer&lt;/td&gt;
&lt;td&gt;Proxy OR Network&lt;/td&gt;
&lt;td&gt;Error distribution; intermediate network status&lt;/td&gt;
&lt;td&gt;Concentrated on specific routes: network/proxy issue&lt;/td&gt;
&lt;td&gt;Check proxy connectivity; test alternate endpoints&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TLS Handshake Failure&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSL/TLS negotiation fails&lt;/td&gt;
&lt;td&gt;Fingerprint Detection OR Config&lt;/td&gt;
&lt;td&gt;Error message details; certificate chain; TLS version mismatch&lt;/td&gt;
&lt;td&gt;Protocol mismatch vs fingerprint detection&lt;/td&gt;
&lt;td&gt;Config: fix TLS settings. Fingerprint: risk boundary reached&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Session Loss&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Auth state lost; cart emptied; workflow restart&lt;/td&gt;
&lt;td&gt;Configuration (rotation mode)&lt;/td&gt;
&lt;td&gt;Rotation mode (sticky vs per-request); workflow type&lt;/td&gt;
&lt;td&gt;Using per-request rotation for stateful workflow&lt;/td&gt;
&lt;td&gt;Switch to sticky session mode&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content Anomaly&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Empty response; blocked page content; unexpected content&lt;/td&gt;
&lt;td&gt;Soft Blocking&lt;/td&gt;
&lt;td&gt;Content hash comparison; content length patterns; comparison across IPs&lt;/td&gt;
&lt;td&gt;Consistent different content across IPs: soft blocking&lt;/td&gt;
&lt;td&gt;Collect content samples; may indicate policy boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Retry Configuration Reference (from Scrapy documentation):&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;RetryMiddleware retries failed requests with configurable retry codes and max retries. Default retry codes include: 500, 502, 503, 504, 522, 524, 408, 429. Note that 429 (rate limit) in retry codes should be combined with backoff strategy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;RETRY_ENABLED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;RETRY_TIMES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;RETRY_HTTP_CODES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;503&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;522&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;524&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;408&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Important Distinction:&lt;/strong&gt; Sudden success rate drops often correlate with target site anti-bot updates rather than proxy quality changes. Before blaming your proxy provider, check whether the target site has updated its bot detection systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stop Conditions and Compliance Boundaries: When to Stop Switching Proxies and Accept Policy Constraints
&lt;/h2&gt;

&lt;p&gt;When diagnostic evidence indicates a policy boundary rather than a proxy quality issue, continued rotation attempts waste resources and may increase legal or compliance risk. This section defines explicit stop conditions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Risk Boundary Box (TEMPLATE)
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Technical Stop Conditions (Proxy Rotation Won't Help)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Evidence to Detect&lt;/th&gt;
&lt;th&gt;Why Rotation Fails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Fingerprint-based detection active&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Block persists across IPs with varied timing; same block pattern regardless of rotation; TLS fingerprint mismatch errors&lt;/td&gt;
&lt;td&gt;Non-IP signals override IP rotation. Advanced bot detection uses behavioral biometrics including mouse movements, scroll patterns, and typing dynamics. Device fingerprinting combines browser, OS, screen, and plugin attributes for identification.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Behavioral analysis blocking&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Challenge triggers on timing patterns; mouse movement verification required; invisible CAPTCHA consistently analyzing behavior&lt;/td&gt;
&lt;td&gt;Mouse/timing patterns tracked across IPs. Invisible CAPTCHA variants analyze behavioral signals before presenting visible challenges.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cookie/session tracking persistent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Same identity tracked despite IP change; cross-request correlation evident&lt;/td&gt;
&lt;td&gt;First-party cookie persistence enables cross-request tracking despite IP changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TLS fingerprint mismatch&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Consistent TLS handshake failures; protocol-level identification patterns&lt;/td&gt;
&lt;td&gt;Protocol-level identification based on TLS fingerprinting (JA3 or similar) persists regardless of IP.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Account-level restrictions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Logged-in account blocked regardless of IP; account-specific rate limits&lt;/td&gt;
&lt;td&gt;User identity tracked independently of IP address.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Policy/Compliance Stop Conditions
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Evidence to Detect&lt;/th&gt;
&lt;th&gt;Compliance Note&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;robots.txt Disallow for target paths&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Target paths explicitly disallowed in robots.txt&lt;/td&gt;
&lt;td&gt;robots.txt provides advisory crawl directives but is not technically enforced by servers. Disallow directives indicate content owner's access preferences, relevant for compliance assessment. Advisory but signals content owner intent.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Terms of Service prohibit automated access&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ToS explicitly prohibits scraping, bots, or automated access&lt;/td&gt;
&lt;td&gt;Violating Terms of Service may constitute unauthorized access under CFAA interpretations. Legal risk present. CFAA creates legal risks for accessing computers without authorization. Van Buren v. US (2021) narrowed CFAA scope but ToS-as-authorization remains contested.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Rate limits explicitly documented&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;API or site documentation specifies rate limits&lt;/td&gt;
&lt;td&gt;Crawl-delay directive requests time between requests but implementation varies by crawler. Exceeding documented limits may constitute abuse.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Geographic access restrictions&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Content restricted by geography; access requires specific jurisdiction&lt;/td&gt;
&lt;td&gt;May implicate local regulations beyond CFAA.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Cost/Efficiency Stop Conditions (TEMPLATE—Thresholds require business context)
&lt;/h4&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Evidence to Detect&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Cost per success exceeds threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;cost_per_success_action above acceptable ROI&lt;/td&gt;
&lt;td&gt;Re-evaluate approach or accept limitation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Success rate below minimum viable&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;success_rate below business-required minimum&lt;/td&gt;
&lt;td&gt;Consider alternative data sources&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Retry rate exceeds efficiency threshold&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;retry_rate indicates diminishing returns&lt;/td&gt;
&lt;td&gt;Assess whether continued attempts are worthwhile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Quantified thresholds for cost/efficiency conditions are not provided in the available documentation. Define based on your business requirements.&lt;/p&gt;

&lt;h4&gt;
  
  
  General Guidance
&lt;/h4&gt;

&lt;p&gt;When any stop condition is met, rotation is unlikely to help. Assess compliance requirements and consider whether the data need justifies alternative approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalation:&lt;/strong&gt; Consult legal counsel for ToS/compliance questions; consult vendor support for technical boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IP Proxy Detection Note:&lt;/strong&gt; When evaluating blocking causes, using a proxy checker online or test proxy online service can help verify basic proxy functionality. However, a proxy ip test confirms only that the proxy routes traffic—it does not test against your specific target's bot detection. Your best rotating residential proxies may pass generic testing while failing on specific sites due to the non-IP detection signals described above.&lt;/p&gt;

&lt;p&gt;For teams evaluating residential proxy options, understanding these boundaries helps determine whether rotating residential proxies unlimited bandwidth offers value for your use case, or whether policy constraints will limit effectiveness regardless of bandwidth allocation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It Together: A Diagnostic Checklist Before You Switch Proxies
&lt;/h2&gt;

&lt;p&gt;Before concluding that you need different rotate proxies, a proxy rotate ip configuration change, or a new proxy rotating ip provider, verify you have completed diagnostic attribution:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Collected evidence:&lt;/strong&gt; Do you have the minimum diagnostic evidence pack fields logged for failing requests?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Classified symptom type:&lt;/strong&gt; Have you identified whether failures are HTTP 4xx, 5xx, CAPTCHA, connection, session, or content anomalies?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Tested attribution:&lt;/strong&gt; Have you verified whether the failure pattern persists across 5+ distinct IPs with varied timing?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Checked configuration:&lt;/strong&gt; Have you verified proxy credentials, session mode (sticky vs rotating), and rate limiting settings?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Evaluated stop conditions:&lt;/strong&gt; Have you checked for fingerprint detection, behavioral analysis, or policy/ToS boundaries?&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you have not completed these steps, proxy switching is premature. If you have completed them and evidence points to proxy quality issues (not policy boundaries), then evaluating the best rotating residential proxies for your specific target is appropriate.&lt;/p&gt;

&lt;p&gt;For legitimate web scraping infrastructure needs, &lt;a href="https://proxy001.com/residential-proxies" rel="noopener noreferrer"&gt;residential proxy services&lt;/a&gt; can provide the IP diversity required—but only after confirming that IP rotation addresses your actual blocking cause. Geographic targeting through &lt;a href="https://proxy001.com/locations" rel="noopener noreferrer"&gt;location-specific endpoints&lt;/a&gt; may help if geo-mismatch is contributing to detection, but will not overcome fingerprint or behavioral detection boundaries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Summary: Attribution Before Action
&lt;/h2&gt;

&lt;p&gt;This diagnostic framework separates "policy/blocking signals" from "proxy-quality limitations" to prevent wasted budget and blind troubleshooting. Key takeaways:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attribution first:&lt;/strong&gt; Use the two-bucket framework and symptom taxonomy to diagnose before switching proxies. Proxy server for web scraping changes only help if the issue is proxy quality—not policy boundaries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measure per-target:&lt;/strong&gt; Headline success rates are not diagnostic. Bucket metrics by target_site, request_path, geo_region, and time_window to identify actual performance patterns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recognize stop conditions:&lt;/strong&gt; When fingerprinting, behavioral analysis, or policy constraints are active, rotation cannot help. Accept policy boundaries rather than escalating costs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Collect evidence systematically:&lt;/strong&gt; The minimum diagnostic evidence pack enables attribution, vendor support escalation, and informed decision-making about whether to change providers or accept limitations.&lt;/p&gt;

&lt;p&gt;The goal is not to find proxies that guarantee no blocks—that guarantee does not exist. The goal is to correctly attribute failure causes so you can make informed decisions about configuration changes, provider evaluation, or acceptance of policy constraints.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>networking</category>
      <category>security</category>
    </item>
  </channel>
</rss>
