<?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: Hong Ye</title>
    <description>The latest articles on DEV Community by Hong Ye (@hongye_dev).</description>
    <link>https://dev.to/hongye_dev</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%2F4246505%2F370f9e60-0c64-4935-a2c4-f7e26ffd4ab6.png</url>
      <title>DEV Community: Hong Ye</title>
      <link>https://dev.to/hongye_dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/hongye_dev"/>
    <language>en</language>
    <item>
      <title>Four layers of bot detection, and why disabling the first one changes nothing</title>
      <dc:creator>Hong Ye</dc:creator>
      <pubDate>Sun, 15 Nov 2026 22:01:37 +0000</pubDate>
      <link>https://dev.to/hongye_dev/four-layers-of-bot-detection-and-why-disabling-the-first-one-changes-nothing-3fnn</link>
      <guid>https://dev.to/hongye_dev/four-layers-of-bot-detection-and-why-disabling-the-first-one-changes-nothing-3fnn</guid>
      <description>&lt;p&gt;Four layers of bot detection, and why disabling the first one changes nothing&lt;/p&gt;

&lt;h1&gt;
  
  
  Body
&lt;/h1&gt;

&lt;p&gt;Most people fighting anti-bot systems do the same thing: they find the check that's failing, they defeat it, and they're surprised when it still doesn't work.&lt;/p&gt;

&lt;p&gt;That's because detection isn't one check. It's four layers, and they fail independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Signature
&lt;/h2&gt;

&lt;p&gt;The crude stuff. &lt;code&gt;navigator.webdriver&lt;/code&gt;, &lt;code&gt;cdc_&lt;/code&gt; variables injected by ChromeDriver, the &lt;code&gt;HeadlessChrome&lt;/code&gt; string in the user agent.&lt;/p&gt;

&lt;p&gt;This is what &lt;code&gt;undetected-chromedriver&lt;/code&gt; fixes, and it's why people think they've solved the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Defeating it takes ten minutes.&lt;/strong&gt; Patching &lt;code&gt;navigator.webdriver&lt;/code&gt; can be done with a single &lt;code&gt;Page.addScriptToEvaluateOnNewDocument&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;If a site still catches you after this, the site is not checking signatures. Stop looking here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: Fingerprint
&lt;/h2&gt;

&lt;p&gt;The browser tells the truth about itself, and the truth is inconsistent.&lt;/p&gt;

&lt;p&gt;A real Chrome has plugins. An automated one usually has an empty plugin array.&lt;/p&gt;

&lt;p&gt;A real screen is 1920x1080 with a window slightly smaller. An automated one often reports a viewport that doesn't match the window.&lt;/p&gt;

&lt;p&gt;WebGL reports a renderer string. On headless it says SwiftShader, or ANGLE with a suspicious driver.&lt;/p&gt;

&lt;p&gt;Fonts installed. Timezone versus IP geolocation. &lt;code&gt;Intl&lt;/code&gt; locale. Canvas hash. Audio context hash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No single item is damning.&lt;/strong&gt; The problem is that in aggregate they form a signature, and signatures are compared against a population.&lt;/p&gt;

&lt;p&gt;If your fingerprint matches 0.01% of real users, you get flagged — even though every individual value looks plausible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is why stealth plugins work less well than they used to.&lt;/strong&gt; They make every value &lt;em&gt;look&lt;/em&gt; normal, but they don't make the combination look &lt;em&gt;common&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The reliable fix here is unglamorous: &lt;strong&gt;run the browser with a real user profile.&lt;/strong&gt; &lt;code&gt;--user-data-dir&lt;/code&gt; pointed at a directory where someone actually browsed. You inherit real plugins, real fonts, real history.&lt;/p&gt;

&lt;p&gt;It beats any stealth library, and it costs nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Behaviour
&lt;/h2&gt;

&lt;p&gt;This is the layer most people never think about, and increasingly the one that matters.&lt;/p&gt;

&lt;p&gt;A real person logging in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Moves the mouse in a curve with deceleration and overshoot&lt;/li&gt;
&lt;li&gt;Takes 200-800ms to move between fields&lt;/li&gt;
&lt;li&gt;Types with inter-key intervals that vary (40-300ms), with occasional pauses&lt;/li&gt;
&lt;li&gt;Makes a typo occasionally and corrects it&lt;/li&gt;
&lt;li&gt;Scrolls in chunks, sometimes back up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An automated login:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;element.click()&lt;/code&gt; teleports the cursor and fires a click in one frame&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;send_keys&lt;/code&gt; types at a constant interval&lt;/li&gt;
&lt;li&gt;No mouse movement at all if you used JS to click&lt;/li&gt;
&lt;li&gt;Scroll is &lt;code&gt;window.scrollTo(0, 5000)&lt;/code&gt; — one jump&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Some sites now weight behaviour above fingerprint.&lt;/strong&gt; Because fingerprints are hard to keep consistent across a fleet, but behaviour is trivially cheap to check.&lt;/p&gt;

&lt;p&gt;Fixing this is not about adding random delays. Random delays are still detectable by distribution — real typing has structure (bursts, pauses before difficult words, correction patterns) that uniform randomness does not.&lt;/p&gt;

&lt;p&gt;The cheap version that works: use &lt;code&gt;mouse.move(x, y, steps=N)&lt;/code&gt; to generate interpolation, and type with a log-normal delay distribution. It's not perfect but it clears the first-order checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Session and reputation
&lt;/h2&gt;

&lt;p&gt;Not about the browser at all.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP reputation (datacenter ranges, VPN exit nodes, prior abuse)&lt;/li&gt;
&lt;li&gt;Cookie age (a brand-new cookie jar logging in is suspicious)&lt;/li&gt;
&lt;li&gt;Login velocity from that ASN&lt;/li&gt;
&lt;li&gt;Whether the account has history&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why your perfectly-stealthed browser still gets challenged on a fresh IP with a fresh cookie jar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And this is the layer you cannot code around.&lt;/strong&gt; You can't fabricate account age.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ordering matters
&lt;/h2&gt;

&lt;p&gt;When something fails, the correct diagnostic order is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Does a real browser from the same IP and same cookie jar work?
   → No: it's Layer 4. Your IP or session is burned.
   → Yes: continue.

2. Does your automation work if you point it at the real user profile?
   → Yes: it was Layer 2. Keep the profile approach.
   → No: continue.

3. Does it work if you slow everything down and add real mouse movement?
   → Yes: it was Layer 3.
   → No: continue.

4. It's Layer 1, or it's not anti-bot at all (you have a logic bug).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Most debugging time is wasted because people start at 4 and work backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest conclusion
&lt;/h2&gt;

&lt;p&gt;If a site has invested seriously in Layers 2-4, defeating it is not a ten-minute fix. It's a maintenance project — profiles to keep warm, proxies to rotate, behaviour to tune, and a re-testing loop because they change things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the thing to tell a client before taking the job.&lt;/strong&gt; Not "I can bypass it," but "here's what bypassing it costs per month to keep working."&lt;/p&gt;

&lt;p&gt;The number of sites where the answer is "don't scrape this, use their API or partner with them" is larger than most people want to admit.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>Your scraper is lying to you (and it's your fault)</title>
      <dc:creator>Hong Ye</dc:creator>
      <pubDate>Sun, 15 Nov 2026 20:05:22 +0000</pubDate>
      <link>https://dev.to/hongye_dev/your-scraper-is-lying-to-you-and-its-your-fault-jg5</link>
      <guid>https://dev.to/hongye_dev/your-scraper-is-lying-to-you-and-its-your-fault-jg5</guid>
      <description>&lt;p&gt;&lt;em&gt;The pipeline described here runs on Hacker News' public API and is open source. It's deliberately small — 200 lines, no dependencies — because the point is the error handling, not the parsing.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
    </item>
  </channel>
</rss>
