<?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: Chris</title>
    <description>The latest articles on DEV Community by Chris (@christhor).</description>
    <link>https://dev.to/christhor</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4116637%2F72d609cb-1a44-4981-b6a3-39b2a98e02fa.jpg</url>
      <title>DEV Community: Chris</title>
      <link>https://dev.to/christhor</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/christhor"/>
    <language>en</language>
    <item>
      <title>Why Rotating Proxies Alone Stopped Working for My Scraper</title>
      <dc:creator>Chris</dc:creator>
      <pubDate>Thu, 10 Sep 2026 02:43:59 +0000</pubDate>
      <link>https://dev.to/christhor/why-rotating-proxies-alone-stopped-working-for-my-scraper-2mpc</link>
      <guid>https://dev.to/christhor/why-rotating-proxies-alone-stopped-working-for-my-scraper-2mpc</guid>
      <description>&lt;p&gt;I spent the better part of last month debugging a scraper that had been running fine for almost a year. The symptom was familiar: requests started returning 403s, then CAPTCHAs, then nothing at all. I did what I always did. I added more IPs to the pool. I shortened the rotation interval. I swapped out the proxy provider entirely.&lt;/p&gt;

&lt;p&gt;It didn't help.&lt;/p&gt;

&lt;p&gt;That's when I realized the thing doing the blocking had changed what it was looking at. And my architecture was built for a problem that no longer existed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Old Model: IP Address Was the Signal
&lt;/h2&gt;

&lt;p&gt;For years, the mental model was simple. A website sees a request coming from an IP address. If that IP has made too many requests too fast, or if the IP belongs to a known datacenter range, it gets blocked. The fix was equally simple: rotate IPs. Use residential proxies, spread requests across thousands of addresses, and the blocking system loses track of you.&lt;/p&gt;

&lt;p&gt;This model still exists in places. Sites with basic rate limiting, simple IP reputation checks, or no behavioral analysis at all. If your target falls into this category, rotation works perfectly fine.&lt;/p&gt;

&lt;p&gt;But the sites that matter — the ones with real data, the ones behind Cloudflare, Akamai, DataDome, or HUMAN — are running something different now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Actually Gets Checked Today
&lt;/h2&gt;

&lt;p&gt;Modern anti-bot systems don't judge a single request in isolation. They evaluate the pattern of behavior across an entire session. A few signals show up repeatedly across these platforms:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Session continuity.&lt;/strong&gt; A real visitor arrives, looks around, and takes a sequence of actions that build on each other. Cookies persist. A session token carries across pages. The same client keeps showing up in a way that looks like one visitor rather than a new stranger every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Browser environment consistency.&lt;/strong&gt; A genuine browser has a stable, internally consistent set of characteristics — rendering behavior, available APIs, hardware-reported details — that stay the same request to request. A scraping setup that swaps configuration on every attempt produces a client that looks like a different device every single request. That inconsistency is itself an anomaly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Timing and pacing.&lt;/strong&gt; Human interaction has natural variability. A script firing requests at a fixed interval, or firing them faster than a person plausibly could, stands out against that baseline.&lt;/p&gt;

&lt;p&gt;The systems combine these signals into something closer to a running confidence score than a single pass/fail check.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With My Architecture
&lt;/h2&gt;

&lt;p&gt;The pattern I was using — one request per IP, rotate on every attempt, discard any notion of session — was built for a world where the IP address was the primary signal. Against a behavioral system, that exact pattern is what stands out.&lt;/p&gt;

&lt;p&gt;A client that shows up once, has no session history, presents a slightly different environment fingerprint than the last "visitor," and repeats this every few seconds looks less like a large number of different humans and more like exactly what it is: one automated process cycling through addresses. The architecture built to solve the old problem actively produces the signal the new problem is designed to catch.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Changed
&lt;/h2&gt;

&lt;p&gt;The shift wasn't about chasing every new fingerprinting technique. That's a losing game. It was about rethinking the shape of the architecture around a few principles.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat sessions, not individual requests, as the unit of work.&lt;/strong&gt; Instead of a new identity for every request, group related requests into a session that persists for a realistic span, carrying its own cookies and state the way a genuine browsing session would.&lt;/p&gt;

&lt;p&gt;Here's what that looks like in practice. Instead of 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="c1"&gt;# What I used to do: new IP every request
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&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="nf"&gt;get_random_proxy&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;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;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&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I switched to a session-based approach:&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="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="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;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;http://user-session-abc123:pass@gate.provider.com:10001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# All requests share the same IP for the session duration
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;urls&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key detail is the &lt;code&gt;session-abc123&lt;/code&gt; parameter in the proxy username. That tells the proxy provider to keep the same exit IP for all requests carrying that session identifier. Sticky sessions like this typically last anywhere from 1 to 30 minutes, depending on the provider and plan.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keep the client environment internally consistent for the life of a session.&lt;/strong&gt; Whatever combination of browser engine, headers, and configuration a session starts with should stay stable for as long as that session lasts. Consistency itself is part of what a legitimate visitor looks like.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use rotation strategically, not reflexively.&lt;/strong&gt; Rotation still has a role — but it should be a deliberate decision tied to identity change, not a background behavior that fires on every request. If a session is blocked, rotate. If it's working, let it run.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Diagnostic Worth Building
&lt;/h2&gt;

&lt;p&gt;One pattern that has helped me repeatedly: when a request fails, find out what actually got blocked before you rotate anything. A simple &lt;code&gt;diagnose()&lt;/code&gt; function that runs before any remediation logic and logs whether the failure was IP-scoped, session-scoped, or something else. Only the IP-scoped branch is allowed to call &lt;code&gt;rotate()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That single pattern would have saved me about two weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Takeaway
&lt;/h2&gt;

&lt;p&gt;If your scraper is getting blocked and your first instinct is still "add more IPs," it's worth asking when that fix last actually worked cleanly. The honest answer for a lot of teams is "a while ago." That doesn't mean proxies are useless — good residential and ISP proxies are still a base requirement. It means the architecture around them has to account for what the blocking system is actually evaluating.&lt;/p&gt;

&lt;p&gt;Sessions, not requests. Consistency, not randomness. Diagnose before you rotate.&lt;/p&gt;

&lt;p&gt;If you're working on something similar and want to compare notes, I'd love to hear what's working for you.&lt;/p&gt;

</description>
      <category>discuss</category>
      <category>beginners</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
