<?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: Flora</title>
    <description>The latest articles on DEV Community by Flora (@thordata-flora).</description>
    <link>https://dev.to/thordata-flora</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%2F3919110%2F3cb37d86-b868-419f-a05b-40c56c7ff6dc.png</url>
      <title>DEV Community: Flora</title>
      <link>https://dev.to/thordata-flora</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thordata-flora"/>
    <language>en</language>
    <item>
      <title>The Hard Part of Video Datasets Isn't the Scraper: Reconciling YouTube, TikTok, and Shorts Into One Schema</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Thu, 17 Sep 2026 09:08:09 +0000</pubDate>
      <link>https://dev.to/thordata-flora/the-hard-part-of-video-datasets-isnt-the-scraper-reconciling-youtube-tiktok-and-shorts-into-one-3chk</link>
      <guid>https://dev.to/thordata-flora/the-hard-part-of-video-datasets-isnt-the-scraper-reconciling-youtube-tiktok-and-shorts-into-one-3chk</guid>
      <description>&lt;p&gt;Every team that says "we want video data for trend analysis" starts the same way. Someone points a scraper at three platforms  -  YouTube, TikTok, and Shorts  -  pulls a few thousand rows, and everything looks fine in the notebook. Three weeks later the pipeline starts lying. Not loudly. It merges rows, drops rows, or produces per-platform engagement numbers that look real but aren't comparable.&lt;/p&gt;

&lt;p&gt;The scraper wasn't the problem. The schema was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why three "video" objects don't fit
&lt;/h2&gt;

&lt;p&gt;Each platform has an internally consistent model of what a video &lt;em&gt;is&lt;/em&gt;, and the differences don't line up cleanly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity.&lt;/strong&gt; YouTube keys on &lt;code&gt;videoId&lt;/code&gt; (11-char base64-ish string) with &lt;code&gt;channelId&lt;/code&gt; for the creator. TikTok keys on a numeric-string &lt;code&gt;video.id&lt;/code&gt; with &lt;code&gt;author.secUid&lt;/code&gt; and &lt;code&gt;author.uniqueId&lt;/code&gt;. Shorts are technically YouTube videos, but a video only becomes a Short if it's vertical and under three minutes  -  the same &lt;code&gt;videoId&lt;/code&gt; can appear in a Shorts feed and the main feed, and each surface has its own URL pattern and a different engagement velocity. Any "cross-platform" table you build ends up needing a canonical concept-level id plus an alias table, not one id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time.&lt;/strong&gt; YouTube's &lt;code&gt;snippet.publishedAt&lt;/code&gt; is RFC 3339 UTC with second precision. TikTok's &lt;code&gt;create_time&lt;/code&gt; is a Unix-seconds integer with no timezone attached, and the same value looks different for a video first posted via a scheduler. Shorts inherit the underlying YouTube publish time even if the video was re-cut and re-uploaded as a Short a month later. Duration is worse: YouTube returns ISO-8601 via &lt;code&gt;contentDetails.duration&lt;/code&gt; (&lt;code&gt;PT1M32S&lt;/code&gt;, which will bite you at &lt;code&gt;PT0S&lt;/code&gt; for live streams and at &lt;code&gt;P1DT2H&lt;/code&gt; for marathons), while TikTok returns integer seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Engagement.&lt;/strong&gt; The set of counters you can pull differs. YouTube exposes views, likes, comment count. TikTok exposes plays, likes, comments, shares, and saves  -  and, since it removed public dislikes, no dislikes at all. Shorts expose "plays" that behave differently from a YouTube "view" even for the same underlying asset. Once you ask "engagement per hour since publish" across the three, you can't naively divide one number into another.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Text.&lt;/strong&gt; YouTube splits &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, and &lt;code&gt;tags&lt;/code&gt; (which are separate from the hashtags a creator drops in the description). TikTok puts everything in one &lt;code&gt;caption&lt;/code&gt; field with hashtags embedded, plus &lt;code&gt;duetInfo&lt;/code&gt; / &lt;code&gt;stitchInfo&lt;/code&gt; flags. Shorts inherit the YouTube text model, but hashtags matter disproportionately in that feed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audio.&lt;/strong&gt; TikTok exposes &lt;code&gt;music.id&lt;/code&gt;, &lt;code&gt;music.original&lt;/code&gt;, &lt;code&gt;music.duration&lt;/code&gt;  -  a sound-driven medium. YouTube has no equivalent field. A viral TikTok clip is often one sound replicated across thousands of videos; without a &lt;code&gt;sound_id&lt;/code&gt; in your normalized row, you can't cluster them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aspect ratio and length bucket.&lt;/strong&gt; 9:16 vs 16:9 is not a cosmetic column. If the dataset feeds a vision model, you'll bucket by aspect ratio before you resize. Mixing 4K landscape with 720p vertical without a bucket column silently degrades training.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shape of a normalized row
&lt;/h2&gt;

&lt;p&gt;After enough rounds of patches, the row I keep has roughly these fields. It isn't universal  -  the point is that it's &lt;em&gt;stable enough&lt;/em&gt; to write queries against.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;canonical_video_id        string    stable pipeline-assigned id
platform                  enum      youtube | tiktok | shorts
platform_native_id        string    videoId / video.id
creator_id                string    channelId / secUid
creator_handle            string    display handle
publish_ts                int64     unix seconds, UTC
duration_s                int32
aspect_ratio              float     e.g. 0.5625 for 9:16
title                     string    caption on TikTok
hashtags                  array&amp;lt;string&amp;gt;
description               string    empty on TikTok
play_count                int64     views on YT, plays on TT/Shorts
like_count                int64
comment_count             int64
share_count               int64     null on YouTube (not exposed)
save_count                int64     null except TikTok
sound_id                  string    TikTok only
is_sound_original         bool      TikTok only
first_seen_ts             int64     when your pipeline saw this row
last_seen_ts              int64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything the platform doesn't give you is &lt;code&gt;null&lt;/code&gt;, never zero. Zero on &lt;code&gt;share_count&lt;/code&gt; means "we observed zero shares"; null means "the platform doesn't expose this". If you skip that rule, per-platform comparisons come out backwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  A normalize function that survives review
&lt;/h2&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;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hashlib&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;isodate&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;parse_duration&lt;/span&gt;   &lt;span class="c1"&gt;# pip install isodate
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;canonical_id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;platform&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;native_id&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;Stable id for a row that may be observed on multiple surfaces (Shorts + YouTube).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sha1&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;platform&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;native_id&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;encode&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()[:&lt;/span&gt;&lt;span class="mi"&gt;16&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;norm_youtube&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;-&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="n"&gt;sn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;snippet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;contentDetails&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;st&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;statistics&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;pub&lt;/span&gt; &lt;span class="o"&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;fromisoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publishedAt&lt;/span&gt;&lt;span class="sh"&gt;"&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;Z&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;+00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;dur_iso&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd&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;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;duration_s&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="nf"&gt;parse_duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dur_iso&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;total_seconds&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;dur_iso&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;desc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sn&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;description&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="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;hashtags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&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;desc&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;platform&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;youtube&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;platform_native_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;resourceId&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;videoId&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;creator_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;channelId&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;creator_handle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sn&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;channelTitle&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;publish_ts&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;pub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;duration_s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aspect_ratio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;          &lt;span class="c1"&gt;# not reliably derivable from v3 API
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&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;hashtags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashtags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;desc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;play_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;st&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;viewCount&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;like_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;likeCount&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;likeCount&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;st&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;comment_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;st&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commentCount&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;commentCount&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;st&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;share_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;save_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;sound_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;is_sound_original&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;norm_tiktok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;-&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="n"&gt;stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;stats&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;ts&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;item&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;create_time&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="n"&gt;caption&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;desc&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="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="n"&gt;hashtags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;#([\w\u4e00-\u9fff]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;video&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;video&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;w&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;video&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;width&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;video&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;height&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;music&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&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;music&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;platform&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;tiktok&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;platform_native_id&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;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&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;creator_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author&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;secUid&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;creator_handle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;author&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;uniqueId&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;publish_ts&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration_s&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;video&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;duration&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="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;# ms -&amp;gt; s
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;aspect_ratio&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;h&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&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;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;caption&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&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="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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hashtags&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;hashtags&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;play_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;stats&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;playCount&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;like_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;stats&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;diggCount&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;comment_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;stats&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;commentCount&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;share_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;stats&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;shareCount&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;save_count&lt;/span&gt;&lt;span class="sh"&gt;"&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="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;collectCount&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;collectCount&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;stats&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;sound_id&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;music&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;id&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;music&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;is_sound_original&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;music&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;original&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;music&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_row&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;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_seen&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;-&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="n"&gt;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;dict&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;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;canonical_video_id&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;canonical_id&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;platform_native_id&lt;/span&gt;&lt;span class="sh"&gt;"&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;first_seen_ts&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="n"&gt;first_seen&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;last_seen_ts&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="n"&gt;first_seen&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small, and not exhaustive. The point is that each &lt;code&gt;norm_*&lt;/code&gt; maps a platform-specific response into the same field names and uses &lt;code&gt;None&lt;/code&gt; for what the platform doesn't expose. Anything downstream (dedup, engagement curves, model input) reads &lt;code&gt;row["like_count"]&lt;/code&gt; and doesn't care where it came from.&lt;/p&gt;

&lt;p&gt;The aliasing that turns &lt;code&gt;platform_native_id&lt;/code&gt; into &lt;code&gt;canonical_video_id&lt;/code&gt; is where most pipelines eventually break. Rule of thumb: keep two ids  -  one for the row you observed on a given surface, one for the &lt;em&gt;concept&lt;/em&gt; ("this piece of content") that may show up on two or three surfaces. A YouTube Short's &lt;code&gt;videoId&lt;/code&gt; is the same as its main-feed &lt;code&gt;videoId&lt;/code&gt;, but you still want those two observations to be joinable at concept level, not deduped into oblivion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that break in week three
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Timezones.&lt;/strong&gt; TikTok's &lt;code&gt;create_time&lt;/code&gt; is nominally UTC-seconds, but tooling sometimes interprets it in the creator's local zone. Fix it at ingest, not at query time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deleted / private videos.&lt;/strong&gt; A row you scraped last week is 404 today. Keep &lt;code&gt;first_seen_ts&lt;/code&gt; and &lt;code&gt;last_seen_ts&lt;/code&gt; on every row, and never overwrite the earliest &lt;code&gt;first_seen&lt;/code&gt;. Your "engagement over time" charts need both columns.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Play-count ceilings.&lt;/strong&gt; TikTok's public &lt;code&gt;playCount&lt;/code&gt; is known to saturate around the 600k mark in some response paths. Treat values at that magnitude as a floor, not a true count.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shorts inheriting YouTube comments.&lt;/strong&gt; If a creator re-edits a landscape video into a vertical Short with the same underlying asset, the comment stream is shared. Dedup comments by comment id, not by row.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;duration&lt;/code&gt; at &lt;code&gt;PT0S&lt;/code&gt; and &lt;code&gt;P1DT2H&lt;/code&gt;.&lt;/strong&gt; Both are technically valid ISO-8601 durations, and both will crash a naive &lt;code&gt;parse_duration(d).seconds&lt;/code&gt;. Use &lt;code&gt;.total_seconds()&lt;/code&gt;, and bucket live vs VOD before aggregating.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags are not hashtags.&lt;/strong&gt; YouTube tags are metadata the creator picks (often keyword-stuffed); hashtags in the description are what viewers actually see. Do not merge them into one column or your topic model will drift.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Aspect ratio isn't in the YouTube v3 API.&lt;/strong&gt; You can infer verticality from duration under 3 minutes plus a channel that ships Shorts, but inference is not measurement. If the pipeline depends on a real aspect ratio, either scrape the player metadata, key it from a video-download pass, or leave it null.&lt;/p&gt;

&lt;h2&gt;
  
  
  Buy or scrape?
&lt;/h2&gt;

&lt;p&gt;If you need today's videos, or one specific creator's corpus, scrape it  -  the field mapping above is essentially the whole job. If you need a historical window across thousands of creators ("all videos that mention brand X in the last 90 days, with their engagement curves"), scraping row-by-row is slower, blockier, and more IP-hungry than you'd want. That's the case where a pre-built multi-platform dataset is a better shape: fewer requests, more coverage per dollar, and you start from a normalized table instead of an ETL job.&lt;/p&gt;

&lt;p&gt;I work with Thordata on their video products (partner disclosure  -  this is not a neutral review), and their &lt;a href="https://www.thordata.com/products/multi-platform-video-datasets?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;multi-platform video datasets&lt;/a&gt; and &lt;a href="https://www.thordata.com/products/video-data-scraper?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;video data scraper&lt;/a&gt; sit on either side of that trade. What I'd actually recommend, though, is running the normalize function above on a small sample from &lt;em&gt;whatever&lt;/em&gt; source you end up with  -  buy, scrape, or a mix  -  before you commit to a schema. The mapping step is the fragile part. Fetch is a boring choice you can make later on cost per row.&lt;/p&gt;

</description>
      <category>datascience</category>
      <category>scraping</category>
      <category>dataset</category>
      <category>python</category>
    </item>
    <item>
      <title>Beyond the Exit IP: How WebRTC and DNS Undo Your Residential Proxy (and How to Verify It)</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Thu, 17 Sep 2026 05:41:14 +0000</pubDate>
      <link>https://dev.to/thordata-flora/beyond-the-exit-ip-how-webrtc-and-dns-undo-your-residential-proxy-and-how-to-verify-it-3jd5</link>
      <guid>https://dev.to/thordata-flora/beyond-the-exit-ip-how-webrtc-and-dns-undo-your-residential-proxy-and-how-to-verify-it-3jd5</guid>
      <description>&lt;p&gt;A residential proxy only controls the path your HTTP requests take. It does not automatically control every way a browser can reveal where it is. That gap is where a surprising number of "clean IP but still blocked" cases come from, and WebRTC is the loudest version of it.&lt;/p&gt;

&lt;p&gt;Here is the failure I see most often in scraping setups that already do IP hygiene right. You route traffic through a residential exit in Berlin. The HTTP layer looks fine: your fetch goes out a Berlin ASN, the site logs a Berlin address, geolocation APIs agree. Then the same page opens an &lt;code&gt;RTCPeerConnection&lt;/code&gt;, fires a couple of STUN binding requests, and the reflexive candidate it gathers reports your real outbound address 鈥?the datacenter box the headless browser actually runs on, or a home broadband IP that is nowhere near Berlin. One request says Berlin, the other says "actually, Frankfurt, AS-NOT-A-RESIDENTIAL." Modern anti-bot does not need to block you on that. It just needs to stop trusting you and quietly downgrade you to a challenge or a thin-data page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the proxy does not govern
&lt;/h2&gt;

&lt;p&gt;When you hand Chromium an HTTP or SOCKS5 proxy, it proxies the connections you make through the normal network stack. WebRTC's ICE gathering is a different animal: the browser tries to discover the addresses the peer can be reached at, and part of that discovery is direct UDP STUN. On many default configurations that UDP path does not respect the proxy. So the IP you &lt;em&gt;want&lt;/em&gt; the page to see and the IP WebRTC &lt;em&gt;thinks&lt;/em&gt; it has diverge.&lt;/p&gt;

&lt;p&gt;There is a wrinkle that makes this less obvious than it used to be. Recent Chromium builds redact local and private ICE candidates behind mDNS 鈥?instead of a plain &lt;code&gt;192.168.x.x&lt;/code&gt; in the candidate list you now get something like &lt;code&gt;a1b2c3.local&lt;/code&gt;. That killed the old-school "print the local IP from WebRTC" trick and made a lot of people think WebRTC was safe now. It is not, for our purpose: the redaction covers &lt;em&gt;local&lt;/em&gt; addresses, but the server-reflexive candidate 鈥?the public address a STUN server saw you come from 鈥?still gets collected unless you stop it. And that public reflexive address is exactly the geo/ASN contradiction that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blunt fix: force WebRTC through the proxy
&lt;/h2&gt;

&lt;p&gt;The supported way to stop non-proxied UDP is the &lt;code&gt;webrtc-ip-handling-policy&lt;/code&gt; switch. &lt;code&gt;disable_non_proxied_udp&lt;/code&gt; tells the browser not to send UDP that is not going through the proxy, which collapses the leak because the direct STUN path is gone.&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;playwright.sync_api&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sync_playwright&lt;/span&gt;

&lt;span class="n"&gt;PROXY&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;server&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://gateway.your-residential-provider.net:8000&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;username&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;cust-session-42&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# session/user encodes the target geo
&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="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="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&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;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&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="n"&gt;headless&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;args&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;--webrtc-ip-handling-policy=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;--disable-features=AsyncDns&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;proxy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;PROXY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;sync_playwright&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;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;launch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;locale&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-DE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone_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;Europe/Berlin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new_page&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example-target.com/listing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait_until&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;domcontentloaded&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 extraction ...
&lt;/span&gt;    &lt;span class="n"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to note in that snippet, because they are where people get bitten. First, &lt;code&gt;disable_non_proxied_udp&lt;/code&gt; may break WebRTC entirely for pages that genuinely need it 鈥?that is fine when you are scraping a listing page, but it is a real behavior change, not free. Second, the flag is not version-proof. Chromium has moved around how command-line WebRTC policies are honored across releases, and enterprise-policy variants (&lt;code&gt;WebRtcIPHandling&lt;/code&gt;) can take precedence. The flag is necessary-but-verify, not set-and-forget. Which is the whole point of the next section.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not trust the fix 鈥?measure it
&lt;/h2&gt;

&lt;p&gt;The honest version of proxy work is that you do not know you closed the leak until you have looked at what the browser actually gathers and compared it to what an HTTP request shows. That test is small and worth putting in CI.&lt;/p&gt;

&lt;p&gt;Run one gather of ICE candidates inside the browser, and one plain HTTP echo of your public IP through the same proxy, and assert they tell a consistent story.&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;JS_GATHER_CANDIDATES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
async () =&amp;gt; {
  const out = [];
  const pc = new RTCPeerConnection({ iceServers: [{ urls: &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;stun:stun.l.google.com:19302&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; }] });
  pc.createDataChannel(&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="s"&gt;);
  pc.onicecandidate = e =&amp;gt; { if (e.candidate) out.push(e.candidate.candidate); };
  const done = new Promise(res =&amp;gt; { pc.onicegatheringstatechange =
      () =&amp;gt; pc.iceGatheringState === &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;complete&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; &amp;amp;&amp;amp; res(); });
  await pc.createOffer().then(o =&amp;gt; pc.setLocalDescription(o));
  // ICE gathering can hang forever behind some proxies; bound it.
  await Promise.race([done, new Promise(r =&amp;gt; setTimeout(r, 4000))]);
  pc.close();
  return out;
}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;http_public_ip&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;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&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="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&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;r&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&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="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="c1"&gt;# Inside a page context after launch():
&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&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="n"&gt;JS_GATHER_CANDIDATES&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;http_ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;http_public_ip&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://gateway.your-residential-provider.net:8000&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP sees:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http_ip&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;c&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="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;candidate:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you are looking for: any candidate line containing &lt;code&gt;host&lt;/code&gt; or &lt;code&gt;srflx&lt;/code&gt; with a public IPv4 that is not the &lt;code&gt;HTTP sees:&lt;/code&gt; address. &lt;code&gt;.local&lt;/code&gt; mDNS entries are the browser redacting a private address and are acceptable; a raw public &lt;code&gt;srflx&lt;/code&gt; that disagrees with the proxy is the leak. If your provider exposes a geo lookup, resolve both IPs and confirm the country (and ideally ASN type) matches. If they diverge, the policy flag did not take on that build, and you are shipping a contradiction to every anti-bot that checks it.&lt;/p&gt;

&lt;p&gt;The same "the proxy is not a magic cloak" logic extends past WebRTC. &lt;code&gt;--disable-features=AsyncDns&lt;/code&gt; matters because Chromium's built-in asynchronous resolver can resolve names directly and leak the DNS query path to your host's resolver, which geolocates to the wrong place even when HTTP is proxied. And a proxy does nothing at all about &lt;code&gt;Intl.DateTimeFormat().resolvedOptions().timeZone&lt;/code&gt;, the WebGL renderer string, or &lt;code&gt;navigator.hardwareConcurrency&lt;/code&gt; 鈥?so pairing a Berlin exit IP with a &lt;code&gt;America/New_York&lt;/code&gt; timezone and a Google cloud GPU is its own tell. IP is one signal; the job is to keep the signals consistent, not to swap one number and call it done.&lt;/p&gt;

&lt;p&gt;Operationally, the cheapest thing you can do is treat the echo-vs-candidate check as a log line instead of a one-off experiment. On every Nth navigation, record the HTTP echo IP, its resolved country and ASN type, and any public &lt;code&gt;srflx&lt;/code&gt; candidate you gathered, then alert when they disagree. A mismatch that shows up once is a flaky STUN path; one that shows up on every request from a given exit is the proxy configuration or the browser build, and you want to catch it before the target's trust score does. The failure is rarely loud 鈥?you get valid-looking HTML with prices hidden, results truncated, or a CAPTCHA only on your best-selling queries 鈥?so the signal has to come from your own instrumentation, not from a non-200.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the override trick is the tempting wrong answer
&lt;/h2&gt;

&lt;p&gt;The internet full of "stealth" snippets shows you overriding &lt;code&gt;window.RTCPeerConnection&lt;/code&gt; with an init script that rewrites or empties the candidate list. It is quick, and it is worse than the flag for a specific reason: a hand-rolled &lt;code&gt;RTCPeerConnection&lt;/code&gt; is trivially detectable. A target can call &lt;code&gt;Function.prototype.toString.call(window.RTCPeerConnection)&lt;/code&gt; and see that the "native" constructor now prints JavaScript instead of &lt;code&gt;[native code]&lt;/code&gt;, or compare the methods it expects against what you left behind. An empty candidate array is also a signature: real browsers on real networks gather &lt;em&gt;something&lt;/em&gt;. You replaced a geo contradiction with a bot-ness contradiction, which is often the more expensive one.&lt;/p&gt;

&lt;p&gt;So the practical ranking, from most to least defensible: honor the policy at the engine level and verify it; if you cannot get the engine policy to hold on your build, run WebRTC through a proxy that actually carries the UDP path; and treat a raw JS monkey-patch as a last resort that itself needs to be made consistent with the rest of the fingerprint. When the whole point is a believable real browser and the target leans hard on these signals, that is also the honest case for using a managed scraping browser that owns the browser-fingerprint-and-IP consistency problem for you, instead of rebuilding it flag by flag and version by version.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I would tell you in person
&lt;/h2&gt;

&lt;p&gt;I write this while partnered with Thordata, so treat the product mention as advertising and the reasoning as the part worth keeping. The reasoning generalizes: your provider's &lt;em&gt;residential&lt;/em&gt; line (&lt;a href="https://www.thordata.com/products/residential-proxies?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;residential proxies&lt;/a&gt;) buys you a plausible exit IP; it does not buy you a plausible &lt;em&gt;browser&lt;/em&gt;. Thordata's &lt;a href="https://www.thordata.com/products/scraping-browser?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;scraping browser&lt;/a&gt; exists for the case where you have decided the fingerprint-consistency tax is bigger than the maintenance tax of doing it yourself 鈥?which, on the targets that actually matter, is usually the right call.&lt;/p&gt;

&lt;p&gt;The takeaway that survives any provider: after you fix the IP, run the candidate-vs-echo test above on your own build. A closed leak you have measured is worth more than a leak you assumed the flag handled. If the two numbers agree, you stopped contradicting yourself across protocols. If they do not, you did not, and the cleanest residential IP in your pool will not save you from it.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>scraping</category>
      <category>proxy</category>
      <category>api</category>
    </item>
    <item>
      <title>That "Residential" IP Might Be a Datacenter in Disguise - A Scriptable Proxy Audit</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Thu, 17 Sep 2026 02:10:38 +0000</pubDate>
      <link>https://dev.to/thordata-flora/that-residential-ip-might-be-a-datacenter-in-disguise-a-scriptable-proxy-audit-4gbn</link>
      <guid>https://dev.to/thordata-flora/that-residential-ip-might-be-a-datacenter-in-disguise-a-scriptable-proxy-audit-4gbn</guid>
      <description>&lt;h2&gt;
  
  
  The scenario nobody plans for
&lt;/h2&gt;

&lt;p&gt;You buy a residential proxy plan. Weeks later your scraper's block rate quietly doubles, and support tells you the pool is "100% real residential IPs, peer-to-peer sourced." You have no way to check that claim, so you either trust it or you churn.&lt;/p&gt;

&lt;p&gt;This is uncomfortable to write about as someone affiliated with a proxy vendor — which is exactly why you should learn to verify it yourself. Proxy type is not a marketing adjective. It is a property of an IP address that anyone can look up, because every public IP is registered to an ASN, and ASNs are classified as hosting providers, ISPs, or mobile carriers at the registry level. If your "residential" exit IPs resolve to Hetzner or OVH address space, no amount of branding changes what the target website sees.&lt;/p&gt;

&lt;p&gt;In this post I'll walk through what actually distinguishes the three main proxy types, then give you a small Python script that samples egress IPs through your gateway and classifies them. It runs in about twenty minutes and uses only &lt;code&gt;requests&lt;/code&gt; plus two free public APIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the distinction matters mechanically
&lt;/h2&gt;

&lt;p&gt;When a site's anti-bot system scores a visiting IP, the single most informative feature is the reputation of the /24 (or /48) block the IP belongs to. And the dominant driver of block-level reputation is the ASN's registered type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Datacenter (hosting) space&lt;/strong&gt; — ASN owned by a cloud or hosting provider: AWS, Google Cloud, Hetzner, OVH, Vultr, DigitalOcean, Contabo, and thousands of smaller ones. Every serious bot-management vendor maintains a hosting-space list. Requests from these blocks get elevated scrutiny almost regardless of behavior. These IPs are fast and cheap, which is why datacenter proxies have a legitimate role (more on that later), but they are not residential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consumer ISP (fixed-line) space&lt;/strong&gt; — ASN owned by Comcast, AT&amp;amp;T, Deutsche Telekom, Telefónica, Jio, and so on. This is what a true residential pool is: end-user devices behind consumer connections. Traffic looks like normal households, so block rates are much lower — at the cost of latency, because your request now traverses real users' links.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile carrier space&lt;/strong&gt; — IPs from T-Mobile, Vodafone, Airtel and similar, typically behind CGNAT. Hundreds or thousands of real users share the same egress IP, so the blocklists are reluctant to poison a single IP. This is the highest-trust tier, and the most expensive per gigabyte.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is a fourth category that shows up in audits: &lt;strong&gt;static ISP proxies&lt;/strong&gt;, which are IPs allocated from consumer ISP space but parked in a rack rather than attached to home devices. They register as "residential type" in most geo databases, behave like residential to many blocklists, and offer datacenter-ish speed. That gray zone is precisely where mislabeling is easiest to sell — and easiest to detect, because the ASN owner is public record.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three independent signals
&lt;/h2&gt;

&lt;p&gt;Any single check can be fooled; three checks agreeing is persuasive.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;ASN owner and type.&lt;/strong&gt; Registry data (RIPE/ARIN/APNIC via whois, or conveniently via &lt;code&gt;ip-api.com&lt;/code&gt;) gives the organization behind the IP. "HETZNER-AS" in your residential plan is a red flag; "COMCAST-7922" in your datacenter plan is a surprise in the other direction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reverse DNS (PTR record).&lt;/strong&gt; Consumer ISPs usually assign customer PTR names (&lt;code&gt;cpe-76-xxx.res.spectrum.com&lt;/code&gt;, &lt;code&gt;pppoe-...&lt;/code&gt;, &lt;code&gt;host-by-...&lt;/code&gt;). Datacenter blocks use provider PTR patterns (&lt;code&gt;static.94.23.44.5.ovh.net&lt;/code&gt;). Many residential IPs have no PTR at all — absence is normal and not a failure signal, but a PTR naming a hosting provider is a strong one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geolocation consistency.&lt;/strong&gt; Compare the city/country your gateway claims to use against what three-party geo databases report for the egress IP. Frequent city-level disagreement means either sloppy pool metadata or traffic exiting somewhere other than what you paid for. Be careful here: consumer-grade geolocation databases genuinely disagree at city level even for honest IPs, so treat this as evidence, not a verdict.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The audit script
&lt;/h2&gt;

&lt;p&gt;Requirements: Python 3.9+, &lt;code&gt;pip install requests&lt;/code&gt;. The free &lt;code&gt;ip-api.com&lt;/code&gt; endpoint is HTTP-only and rate-limited to about 450 requests/minute, so we sleep between samples.&lt;/p&gt;

&lt;p&gt;A quick manual check first — before you need the whole script:&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;# what IP do I actually egress as?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; http://USER:PASS@gateway.thordata.com:9999 https://api.ipify.org?format&lt;span class="o"&gt;=&lt;/span&gt;json

&lt;span class="c"&gt;# what does the registry think that IP is?&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"http://ip-api.com/json/203.0.113.42?fields=status,country,city,isp,org,as"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;org&lt;/code&gt; field says anything resembling a hosting provider while you're paying for residential, you already have your answer. The script below automates a statistically meaningful version of that check: sample N egress IPs, look up each one, classify, and report the distribution.&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="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;proxy_audit.py — sample egress IPs through a gateway and classify pool composition.

Usage:
  python proxy_audit.py --proxy http://USER:PASS@GATEWAY:PORT --sessions 30
Most residential/ISP gateways rotate the exit IP per connection (or per
minute); if your gateway supports sticky sessions, do NOT enable them here,
or you will measure one IP thirty times.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;socket&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;Counter&lt;/span&gt;

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

&lt;span class="n"&gt;DATACENTER_KEYS&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;hetzner&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;ovh&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;vultr&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;digitalocean&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;contabo&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;scaleway&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;amazon&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;microsoft&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;google cloud&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;fastly&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;cloudflare&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;akamai&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;equinix&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; Choopa&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;linode&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;upcloud&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;xtom&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;serverius&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;pskz&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;hostkey&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;bluevps&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;MOBILE_KEYS&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;t-mobile&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;telekom mobile&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;vodafone&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;orange&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;airtel&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;jio&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;china mobile&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;china unicom&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;verizon wireless&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;at&amp;amp;ts&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;classify&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="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ptr&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="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="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;blob&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;meta&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;org&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="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;meta&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="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;ptr&lt;/span&gt; &lt;span class="ow"&gt;or&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;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="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MOBILE_KEYS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mobile-carrier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DATACENTER_KEYS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datacenter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="c1"&gt;# An ASN owned by a known consumer ISP with no hosting keyword.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;blob&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;comcast&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;spectrum&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;att&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;verizon&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;telefonica&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;movistar&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;deutsche telekom&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;bt &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;telecom&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;broadband&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;cable&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;consumer-isp (residential-like)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unclassified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;   &lt;span class="c1"&gt;# be honest: unknown is not a verdict
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reverse_dns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&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="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="k"&gt;try&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;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gethostbyaddr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&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;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;herror&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;socket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gaierror&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;OSError&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;span class="c1"&gt;# no PTR — common for real residential IPs
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_once&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="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;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;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="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="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;25&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="nf"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;ip&lt;/span&gt; &lt;span class="o"&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;ip&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="nb"&gt;Exception&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;egress failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&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="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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;m&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="sa"&gt;f&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?fields=status,country,regionName,city,isp,org,as&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;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;Exception&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;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;ip&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lookup failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&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="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="n"&gt;ptr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverse_dns&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&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;ip&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;m&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;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;m&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;country&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;as&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&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;as&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;org&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;m&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;org&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;ptr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;class&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ptr&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;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;ap&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ap&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&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&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;help&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://user:pass@host: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;ap&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;--sessions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&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;default&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="n"&gt;ap&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;--sleep&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&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;default&lt;/span&gt;&lt;span class="o"&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="n"&gt;ap&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;--out&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;audit_results.jsonl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&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;tally&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nc"&gt;Counter&lt;/span&gt;&lt;span class="p"&gt;()&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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&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;f&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;args&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;row&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sample_once&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&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;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;sample&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="n"&gt;i&lt;/span&gt;
            &lt;span class="n"&gt;rows&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;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&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="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tally&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;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;class&lt;/span&gt;&lt;span class="sh"&gt;"&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;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;error&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&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="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;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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;3&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;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;ip&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;—&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;16&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;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;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="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="mi"&gt;14&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;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;class&lt;/span&gt;&lt;span class="sh"&gt;'&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;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;error&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;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;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# respect ip-api free-tier limits
&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;=== pool composition ===&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;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tally&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;most_common&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="mi"&gt;4&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="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sessions&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="n"&gt;f&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;k&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;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="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Raw per-IP evidence saved to &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; — keep it; it is the &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
          &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receipt you show your provider when something looks off.&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;__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="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A couple of implementation notes that will save you a debugging cycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass the proxy under both &lt;code&gt;"http"&lt;/code&gt; and &lt;code&gt;"https"&lt;/code&gt; keys in the dict — &lt;code&gt;requests&lt;/code&gt; needs the explicit &lt;code&gt;https&lt;/code&gt; entry to tunnel TLS over the proxy, and &lt;code&gt;api.ipify.org&lt;/code&gt; is HTTPS-only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;socket.gethostbyaddr&lt;/code&gt; throwing is &lt;em&gt;expected behavior&lt;/em&gt; for many genuine residential IPs (no reverse record configured). Only treat a PTR that names a hosting provider as adverse evidence.&lt;/li&gt;
&lt;li&gt;The keyword lists above are a starting point, not a taxonomy. When the script prints &lt;code&gt;unclassified&lt;/code&gt;, look up that ASN's whois once by hand; registry "status" fields (&lt;code&gt;ASSIGNED PI&lt;/code&gt; vs &lt;code&gt;LEGACY&lt;/code&gt; vs allocatable-to-subscribers) tell you more than any keyword list.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reading the results
&lt;/h2&gt;

&lt;p&gt;What I'd expect a clean run to look like: a residential pool dominated by &lt;code&gt;consumer-isp&lt;/code&gt; with a tail of &lt;code&gt;unclassified&lt;/code&gt; (that's normal — there are ~60,000 consumer ISPs worldwide, far more than any keyword list covers). Now the failure signatures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Majority &lt;code&gt;datacenter&lt;/code&gt; on a residential plan.&lt;/strong&gt; Either you were resold hosting space, or a mixed pool is configured to prefer the cheap half. Take the JSONL evidence to support with specific IPs and timestamps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A stable handful of IPs repeating across samples.&lt;/strong&gt; Two causes: sticky-session mode left on (fix your gateway config), or the pool genuinely has a few /24s it keeps reusing — which is also worth knowing, because reuse concentrates blocklists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;City mismatch rate above roughly 20%.&lt;/strong&gt; Escalate gently; geo DB lag is real, but systemic country-level mismatch never is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mobile plan full of &lt;code&gt;consumer-isp&lt;/code&gt;.&lt;/strong&gt; Mobile proxies are priced at a premium for the carrier-CGNAT property; fixed-ISP IPs in that pool means you are overpaying.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And to be fair about the gray zone: &lt;strong&gt;static ISP proxies&lt;/strong&gt; legitimately occupy the middle. They come from consumer-ISP ASN space (so this audit will happily classify them "residential-like") even though they sit in a rack. If you want datacenter reliability at residential-ASN reputation — dashboard monitoring, ad verification where sessions must persist for weeks — that's the right product, and the pricing gap is large: Thordata, for example, lists its ISP line starting at &lt;strong&gt;$2 per IP&lt;/strong&gt; (fetched from their pricing page on 2026-09-17), a fraction of typical residential per-GB economics. Just buy it knowingly, not by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pools lie in both directions
&lt;/h2&gt;

&lt;p&gt;The uncomfortable lesson from running audits is that labeling errors go both ways: some "enterprise residential" pools are 70% cloud, and some "cheap datacenter" pools turn out to hold real ISP space someone sold as shared hosting by mistake. The only durable response is to make verification part of your pipeline — sample N exits after each provider change, keep the JSONL, and diff the composition monthly. It costs you thirty lines of cron and thirty minutes of attention, and it converts every vague suspicion into a specific, fixable ticket.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work with Thordata (sponsored account). The audit script above uses no Thordata-specific tooling — run it against any vendor's gateway. If you want to see what our residential pool composition looks like under this exact script, the product page is here: &lt;a href="https://www.thordata.com/products/residential-proxies?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/products/residential-proxies?ls=dev&amp;amp;lk=DEV&lt;/a&gt; — and new signups get a trial allowance, no code needed.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>scraping</category>
      <category>proxy</category>
      <category>python</category>
    </item>
    <item>
      <title>Residential proxies: what to measure before scaling</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Thu, 17 Sep 2026 02:00:13 +0000</pubDate>
      <link>https://dev.to/thordata-flora/residential-proxies-what-to-measure-before-scaling-4mdm</link>
      <guid>https://dev.to/thordata-flora/residential-proxies-what-to-measure-before-scaling-4mdm</guid>
      <description>&lt;h2&gt;
  
  
  Residential proxies: a production evaluation guide
&lt;/h2&gt;

&lt;p&gt;Proxy selection is often reduced to a question about IP volume or advertised speed. That is not enough for a production data workflow. The right proxy type depends on the target geography, session model, request pattern, failure tolerance and the value of each successful record.&lt;/p&gt;

&lt;p&gt;This guide explains how to evaluate the infrastructure before scaling a compliant public-web data project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Match the proxy type to the workflow
&lt;/h2&gt;

&lt;p&gt;Residential proxies are useful when location diversity and natural traffic patterns matter. Test geography, session stability and successful results before scaling.&lt;/p&gt;

&lt;p&gt;Residential proxies are generally useful when geographic diversity and residential network context matter. ISP proxies can be useful for repeatable sessions and predictable connectivity. Mobile proxies are relevant when the workflow needs mobile-network context. Datacenter proxies are often attractive for speed and controlled large-volume workloads.&lt;/p&gt;

&lt;p&gt;The choice should be made from measured results, not from the product name alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build a controlled test
&lt;/h2&gt;

&lt;p&gt;Use a small, permitted set of public URLs that represents the real workload. Keep the test constant while changing only one variable at a time:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ext&lt;br&gt;
target URLs ? proxy type ? country ? session mode ? request rate ? result validation&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For each request, record the timestamp, target country, proxy type, session identifier, response status, response time, retry count and whether the returned data is complete. Do not store credentials or personal information in the test logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Measure the metrics that affect cost
&lt;/h2&gt;

&lt;p&gt;At minimum, calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Success rate = successful usable results / total attempts&lt;/li&gt;
&lt;li&gt;Median and P95 response time&lt;/li&gt;
&lt;li&gt;Retry rate&lt;/li&gt;
&lt;li&gt;Session break rate&lt;/li&gt;
&lt;li&gt;Geographic accuracy rate&lt;/li&gt;
&lt;li&gt;Cost per usable result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last metric is usually more useful than cost per gigabyte. A low price is not efficient if failed requests create retries, manual review or incomplete downstream records.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Handle failure explicitly
&lt;/h2&gt;

&lt;p&gt;Use bounded retries with exponential backoff. Stop retrying when the error indicates an invalid request or a policy restriction. Separate transport failures, timeouts, empty results and parsing failures so that one generic retry loop does not hide the real problem.&lt;/p&gt;

&lt;p&gt;A basic workflow should also include a circuit breaker: if the success rate for a location falls below an agreed threshold, pause that route and alert the operator instead of increasing traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Scale gradually
&lt;/h2&gt;

&lt;p&gt;Move from a small test to a limited production run, then compare the same metrics again. Introduce concurrency gradually and watch response time, error rate, bandwidth and downstream parser quality. Keep a rollback path so a target, location or route can be paused without stopping the entire pipeline.&lt;/p&gt;

&lt;p&gt;Today's related industry signal is &lt;strong&gt;reliable proxy infrastructure and practical AI data workflows&lt;/strong&gt;. It is a reminder that data collection infrastructure needs operational measurement as well as a working API.&lt;/p&gt;

&lt;p&gt;Thordata reference: &lt;a href="https://www.thordata.com/blog/ai-trends" rel="noopener noreferrer"&gt;https://www.thordata.com/blog/ai-trends&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Responsible use
&lt;/h2&gt;

&lt;p&gt;Use proxy infrastructure only for lawful, compliant workflows involving permitted public data. Respect applicable laws, website terms, robots directives, rate limits and opt-out requests. Do not use it for credential testing, fake accounts, click manipulation, spam, private-data collection or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Try Thordata: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Rotating Residential Proxies Without Losing Your Mind: 407 Auth, Sticky Sessions, and Backoff in Practice</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Wed, 16 Sep 2026 09:16:18 +0000</pubDate>
      <link>https://dev.to/thordata-flora/rotating-residential-proxies-without-losing-your-mind-407-auth-sticky-sessions-and-backoff-in-122n</link>
      <guid>https://dev.to/thordata-flora/rotating-residential-proxies-without-losing-your-mind-407-auth-sticky-sessions-and-backoff-in-122n</guid>
      <description>&lt;h2&gt;
  
  
  The problem nobody tells you about
&lt;/h2&gt;

&lt;p&gt;Most scraping tutorials stop at "set &lt;code&gt;proxies={'http': ...}&lt;/code&gt; and go." That works for a weekend project pulling a few hundred pages. It falls apart the moment you scale: you start seeing &lt;code&gt;407 Proxy Authentication Required&lt;/code&gt;, mysterious &lt;code&gt;Connection reset by peer&lt;/code&gt;, 429s that arrive in bursts, and — worst of all — silently degraded data where your parser returns an empty list because the site served you a captcha page with a &lt;code&gt;200 OK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Rotating residential proxies fix the IP-reputation half of this, but they introduce their own failure modes. This post walks through the four that actually bite in production, with runnable Python you can lift into your own pipeline.&lt;/p&gt;

&lt;p&gt;I'll use a generic residential gateway model (host + port + username + password) because that is how nearly every provider of this product category, including the residential proxies I run most of my scraping through at &lt;a href="https://www.thordata.com/products/residential-proxies" rel="noopener noreferrer"&gt;Thordata&lt;/a&gt;, hands you credentials. The code is provider-agnostic — swap in your own gateway host and auth string.&lt;/p&gt;

&lt;h2&gt;
  
  
  How residential rotation actually works
&lt;/h2&gt;

&lt;p&gt;A residential proxy network puts you behind real ISP-assigned devices. You don't get one IP; you get a gateway that maps your request onto a pool of residential IPs. The two knobs that matter are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Rotation trigger.&lt;/strong&gt; Either every new connection (fresh exit IP per request) or &lt;em&gt;sticky&lt;/em&gt; — the same exit IP held for a window (often 1–30 minutes) so a multi-step flow (login → add to cart → checkout) doesn't look like it's teleporting across a country between calls.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Targeting.&lt;/strong&gt; Country / city / ISP selection. Tighter targeting = smaller pool = higher chance two of your workers collide on the same residential IP.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The golden rule: &lt;strong&gt;rotate for breadth, stick for depth.&lt;/strong&gt; Collecting 50,000 independent product pages? Rotate every request. Filling out a paginated listing that sets a session cookie? Stick to one IP for that logical "session."&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 1: &lt;code&gt;407 Proxy Authentication Required&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The single most common first-hour error. It almost always means the credentials weren't attached to the request. Two gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some gateways authenticate by &lt;strong&gt;IP allowlist&lt;/strong&gt;, not user/pass. If you configured user/pass but your egress IP isn't allowlisted, you still get 407.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;requests&lt;/code&gt; library only sends proxy auth when the URL scheme matches. &lt;code&gt;http://&lt;/code&gt; proxy URLs are used for &lt;code&gt;http://&lt;/code&gt; targets; if you forget the &lt;code&gt;https&lt;/code&gt; proxy, TLS requests bypass auth and fail differently.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's a minimal, correct example with &lt;code&gt;requests&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;requests&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;gateway.provider.example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9000&lt;/span&gt;

&lt;span class="c1"&gt;# Rotation is often encoded in the username itself.
# A "zone" token or session id appended to the user string selects
# sticky vs rotating behavior on many residential gateways.
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;proxy_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session_id&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="bp"&gt;None&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="nb"&gt;dict&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;BASE_USER&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="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;BASE_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="c1"&gt;# sticky: same IP for window
&lt;/span&gt;    &lt;span class="n"&gt;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;BASE_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="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PORT&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;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;url&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;BASE_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-user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;BASE_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-pass&lt;/span&gt;&lt;span class="sh"&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;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;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="nf"&gt;proxy_for&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;           &lt;span class="c1"&gt;# rotating
&lt;/span&gt;    &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="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="n"&gt;status_code&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="c1"&gt;# confirms the exit IP changed per call
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth internalizing from that snippet:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;timeout&lt;/code&gt; is a &lt;strong&gt;tuple&lt;/strong&gt; &lt;code&gt;(connect, read)&lt;/code&gt;. Residential exits can hang on connect; a bare &lt;code&gt;timeout=20&lt;/code&gt; won't cut off a connection that never completes the handshake as cleanly.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;https&lt;/code&gt; key in the proxies dict is what makes TLS requests carry auth. Omit it and half your requests silently skip the proxy.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure mode 2: sticky sessions done right
&lt;/h2&gt;

&lt;p&gt;For a multi-step flow, generate one session token per logical session and reuse it for every request in that session. The token is what the gateway hashes to pick (and hold) a residential 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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run_multi_step_flow&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;sid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;secrets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token_hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# stable across the flow
&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;proxy_for&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="n"&gt;sid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# same exit IP until window expires
&lt;/span&gt;    &lt;span class="n"&gt;s&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;s&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="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&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="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 ...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;home&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&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://shop.example/&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="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;home&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="c1"&gt;# cookie jar keeps the site-side session; sid keeps the IP-side session
&lt;/span&gt;    &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;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;https://shop.example/cart&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="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;id&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;abc&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="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="n"&gt;cart&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;home&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="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trap: the sticky window is server-side. If your flow takes longer than the window (say 30 minutes of retries), the IP rotates mid-flow and the target site flags the discontinuity. Keep flows short, and if a step fails hard, &lt;strong&gt;start a new session id&lt;/strong&gt; rather than retrying on the stale one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure mode 3: concurrency and the collision problem
&lt;/h2&gt;

&lt;p&gt;Residential pools are finite per geo. Fire 100 concurrent workers all targeting "United States" and two workers get the same residential IP; the target site's rate limiter sees a single very busy user and throttles you. This is where async shines — but you must bound concurrency.&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aiohttp&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="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;GATEWAY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gateway.provider.example&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9000&lt;/span&gt;
&lt;span class="n"&gt;USER&lt;/span&gt;&lt;span class="p"&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-user&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;your-pass&lt;/span&gt;&lt;span class="sh"&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&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;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# rotating: no session token -&amp;gt; fresh residential IP per connection
&lt;/span&gt;    &lt;span class="n"&gt;proxy&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="s"&gt;:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PORT&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;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="n"&gt;proxy&lt;/span&gt;&lt;span class="o"&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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientTimeout&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="mi"&gt;25&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;r&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&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;503&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientResponseError&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="n"&gt;request_info&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="n"&gt;history&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="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;throttled&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="k"&gt;await&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;text&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;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sem&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;urls&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;u&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="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;sem&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;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="mi"&gt;4&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;results&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&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;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;break&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;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientError&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="nb"&gt;TimeoutError&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;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="c1"&gt;# exponential backoff + jitter: never hammer a 429
&lt;/span&gt;                    &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;)&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;uniform&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="mf"&gt;0.5&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;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="n"&gt;delay&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&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="n"&gt;u&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="c1"&gt;# exhausted, mark and move on
&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;urls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sem&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n_workers&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="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&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;i&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;n_workers&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;n_workers&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;aiohttp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ClientSession&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;session&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;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="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sem&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;c&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunks&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;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;sample&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://target.example/item/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&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;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;500&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;t0&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="n"&gt;out&lt;/span&gt; &lt;span class="o"&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;span class="n"&gt;sample&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="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&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;v&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ok&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ok in &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="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;t0&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;s&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;Key points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n_workers&lt;/code&gt; is your dial. Start at 10–20 for residential and &lt;strong&gt;watch for 429s&lt;/strong&gt; — that's your ceiling. Datacenter proxies tolerate far more; residential never does, because real residential IPs get rate-limited like real humans.&lt;/li&gt;
&lt;li&gt;Backoff with &lt;strong&gt;jitter&lt;/strong&gt;, not fixed sleeps. Synchronized retries across workers recreate the very burst that got you throttled.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;total=25&lt;/code&gt; bounds each request; without it, a single hung residential exit stalls a worker forever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Failure mode 4: silent success (the one that ruins data)
&lt;/h2&gt;

&lt;p&gt;The scariest failure is a &lt;code&gt;200 OK&lt;/code&gt; that isn't real content — a captcha, an "access denied" interstitial, or a stub page. &lt;code&gt;status_code == 200&lt;/code&gt; is not "I got the data."&lt;/p&gt;

&lt;p&gt;Guard every response with a shape assertion, and on mismatch, &lt;strong&gt;rotate&lt;/strong&gt; (don't stick — a captcha means this residential IP is now known):&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;is_real_product_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&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;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# cheap structural probe; adapt to your target
&lt;/span&gt;    &lt;span class="nf"&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;data-price&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;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captcha&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;html&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;def&lt;/span&gt; &lt;span class="nf"&gt;scrape_with_shape_retry&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;max_rotations&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="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;max_rotations&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;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="nf"&gt;proxy_for&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="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&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="c1"&gt;# rotating each try
&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="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="nf"&gt;is_real_product_page&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="n"&gt;text&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;# this URL is poisoning your dataset — quarantine it
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quarantine failures to a side list and re-run them later at lower concurrency. A batch that keeps failing shape checks on rotating residential often needs a different geo — the pool in that region is simply "hot" (over-used) that hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  Results and the坑 (pitfalls) I keep hitting
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;DNS leaks.&lt;/strong&gt; Some proxies resolve DNS outside the tunnel, so the target sees your real resolver location even though the HTTP request came from a residential IP. Route DNS through the proxy or verify with a WebRTC/DNS leak test before trusting a run.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS fingerprinting.&lt;/strong&gt; Even with a clean residential IP, a &lt;code&gt;python-requests/2.x&lt;/code&gt; TLS handshake screams automation. If a site is aggressive, rotate through a client that mimics browser TLS (e.g. &lt;code&gt;curl_cffi&lt;/code&gt; with &lt;code&gt;impersonate="chrome"&lt;/code&gt;) — the IP alone won't save you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NTP drift on backoff&lt;/strong&gt; is a non-issue, but &lt;strong&gt;retry budgets&lt;/strong&gt; are: always cap total attempts per URL, or one flaky endpoint can starve your whole worker pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing is usage-based per GB on residential&lt;/strong&gt;, and captcha pages you accidentally download still burn bandwidth. The shape-check above is also a cost control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For per-GB tiers and geo availability, check the current numbers on the &lt;a href="https://www.thordata.com/pricing/residential-proxies" rel="noopener noreferrer"&gt;residential pricing page&lt;/a&gt; rather than trusting any figure I'd quote — these change and I'd rather point you at the live table than write down a number that goes stale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it together
&lt;/h2&gt;

&lt;p&gt;The mental model is small: &lt;strong&gt;rotate for breadth, stick for depth, bound concurrency, assert shape, back off with jitter, and never trust a 200.&lt;/strong&gt; Get those five habits and a residential-proxy pipeline stops being a slot machine and starts being a data line.&lt;/p&gt;

&lt;p&gt;If you want a residential pool that exposes exactly these two knobs (a rotating zone and a sticky-session token in the username) with per-GB billing and no forced minimums to experiment, that's the &lt;a href="https://www.thordata.com/products/residential-proxies" rel="noopener noreferrer"&gt;residential proxies&lt;/a&gt; I reach for, and the same gateway shape means the code above drops in with just your host and auth. New accounts usually get a small free allowance to validate a geo before committing — grab it from the site and run your first 500 URLs through the &lt;code&gt;main()&lt;/code&gt; above to calibrate &lt;code&gt;n_workers&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>scraping</category>
      <category>proxy</category>
      <category>python</category>
    </item>
    <item>
      <title>How to Reduce Proxy Costs Without Sacrificing Data Quality</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Wed, 16 Sep 2026 09:00:10 +0000</pubDate>
      <link>https://dev.to/thordata-flora/how-to-reduce-proxy-costs-without-sacrificing-data-quality-22n0</link>
      <guid>https://dev.to/thordata-flora/how-to-reduce-proxy-costs-without-sacrificing-data-quality-22n0</guid>
      <description>&lt;h2&gt;
  
  
  How to Reduce Proxy Costs Without Sacrificing Data Quality
&lt;/h2&gt;

&lt;p&gt;The cheapest proxy is not always the lowest-cost option. A failed request can create retry traffic, parser work, manual review and delayed data delivery.&lt;/p&gt;

&lt;p&gt;Start by separating three metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost per request&lt;/li&gt;
&lt;li&gt;Cost per successful response&lt;/li&gt;
&lt;li&gt;Cost per validated usable record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third metric is the one that matters most for a production workflow. Measure it by proxy type, country, session mode and target category.&lt;/p&gt;

&lt;p&gt;Use a bounded test with representative public URLs. Keep the parser and required fields constant, then compare residential, ISP, mobile and datacenter routes where appropriate. Track response time, retry rate, session breaks, missing fields and duplicate records.&lt;/p&gt;

&lt;p&gt;Scale the route with the best usable-record economics, not simply the lowest advertised unit price. If a route starts producing incomplete results, reduce concurrency and review the failure category before increasing traffic.&lt;/p&gt;

&lt;p&gt;Thordata provides residential, ISP, mobile and datacenter proxies for compliant ecommerce research, SEO monitoring, market research and localization testing. Limited free trial access can be used to validate the workflow before scaling.&lt;/p&gt;

&lt;p&gt;Use proxy infrastructure responsibly. Do not use it for private-data collection, fake accounts, spam, click manipulation, credential testing or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Start testing: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Choose a Proxy for a Production Web Data Workflow</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Wed, 16 Sep 2026 06:00:12 +0000</pubDate>
      <link>https://dev.to/thordata-flora/how-to-choose-a-proxy-for-a-production-web-data-workflow-482g</link>
      <guid>https://dev.to/thordata-flora/how-to-choose-a-proxy-for-a-production-web-data-workflow-482g</guid>
      <description>&lt;h2&gt;
  
  
  How to Choose a Proxy for a Production Web Data Workflow
&lt;/h2&gt;

&lt;p&gt;Choosing a proxy by IP count or headline price is rarely enough. A production workflow should match the proxy type to the target, geography, session model and quality requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the workflow
&lt;/h2&gt;

&lt;p&gt;Residential proxies can support geographic diversity and residential network context. ISP proxies can support stable, repeatable sessions. Mobile proxies provide mobile-network context. Datacenter proxies can be useful for fast and controlled workloads when the source and intended use allow them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure usable results
&lt;/h2&gt;

&lt;p&gt;Run a bounded test with representative public URLs. Record location accuracy, response time, session stability, retry rate, data completeness and cost per usable result. A request that returns HTTP 200 but misses the required fields is not a successful business result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a controlled rollout
&lt;/h2&gt;

&lt;p&gt;Start with a small workload, bounded concurrency and limited retries. Increase volume only after the route produces repeatable results. Keep separate failure categories for timeouts, empty results, parser failures and policy restrictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test before scaling
&lt;/h2&gt;

&lt;p&gt;Thordata offers residential, ISP, mobile and datacenter proxies for compliant web data workflows. A free trial is available for testing; check the current plan and usage terms before starting a production workload.&lt;/p&gt;

&lt;p&gt;Use the trial to compare the proxy type, country and session mode that fit your actual workflow rather than choosing only by the advertised IP count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsible use
&lt;/h2&gt;

&lt;p&gt;Use proxy infrastructure for lawful workflows such as public ecommerce research, SEO monitoring, market research, advertising verification and localization testing. Do not use it for credential testing, fake accounts, click manipulation, spam, private-data collection or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Start a Thordata trial: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Datacenter proxies: what to measure before scaling</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Wed, 16 Sep 2026 02:00:13 +0000</pubDate>
      <link>https://dev.to/thordata-flora/datacenter-proxies-what-to-measure-before-scaling-djf</link>
      <guid>https://dev.to/thordata-flora/datacenter-proxies-what-to-measure-before-scaling-djf</guid>
      <description>&lt;h2&gt;
  
  
  Datacenter proxies: a production evaluation guide
&lt;/h2&gt;

&lt;p&gt;Proxy selection is often reduced to a question about IP volume or advertised speed. That is not enough for a production data workflow. The right proxy type depends on the target geography, session model, request pattern, failure tolerance and the value of each successful record.&lt;/p&gt;

&lt;p&gt;This guide explains how to evaluate the infrastructure before scaling a compliant public-web data project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Match the proxy type to the workflow
&lt;/h2&gt;

&lt;p&gt;Datacenter proxies are often a fast and cost-efficient option for large workloads. Measure success rate and retry cost alongside raw response speed.&lt;/p&gt;

&lt;p&gt;Residential proxies are generally useful when geographic diversity and residential network context matter. ISP proxies can be useful for repeatable sessions and predictable connectivity. Mobile proxies are relevant when the workflow needs mobile-network context. Datacenter proxies are often attractive for speed and controlled large-volume workloads.&lt;/p&gt;

&lt;p&gt;The choice should be made from measured results, not from the product name alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build a controlled test
&lt;/h2&gt;

&lt;p&gt;Use a small, permitted set of public URLs that represents the real workload. Keep the test constant while changing only one variable at a time:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ext&lt;br&gt;
target URLs ? proxy type ? country ? session mode ? request rate ? result validation&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For each request, record the timestamp, target country, proxy type, session identifier, response status, response time, retry count and whether the returned data is complete. Do not store credentials or personal information in the test logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Measure the metrics that affect cost
&lt;/h2&gt;

&lt;p&gt;At minimum, calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Success rate = successful usable results / total attempts&lt;/li&gt;
&lt;li&gt;Median and P95 response time&lt;/li&gt;
&lt;li&gt;Retry rate&lt;/li&gt;
&lt;li&gt;Session break rate&lt;/li&gt;
&lt;li&gt;Geographic accuracy rate&lt;/li&gt;
&lt;li&gt;Cost per usable result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last metric is usually more useful than cost per gigabyte. A low price is not efficient if failed requests create retries, manual review or incomplete downstream records.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Handle failure explicitly
&lt;/h2&gt;

&lt;p&gt;Use bounded retries with exponential backoff. Stop retrying when the error indicates an invalid request or a policy restriction. Separate transport failures, timeouts, empty results and parsing failures so that one generic retry loop does not hide the real problem.&lt;/p&gt;

&lt;p&gt;A basic workflow should also include a circuit breaker: if the success rate for a location falls below an agreed threshold, pause that route and alert the operator instead of increasing traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Scale gradually
&lt;/h2&gt;

&lt;p&gt;Move from a small test to a limited production run, then compare the same metrics again. Introduce concurrency gradually and watch response time, error rate, bandwidth and downstream parser quality. Keep a rollback path so a target, location or route can be paused without stopping the entire pipeline.&lt;/p&gt;

&lt;p&gt;Today's related industry signal is &lt;strong&gt;reliable proxy infrastructure and practical AI data workflows&lt;/strong&gt;. It is a reminder that data collection infrastructure needs operational measurement as well as a working API.&lt;/p&gt;

&lt;p&gt;Thordata reference: &lt;a href="https://www.thordata.com/blog/ai-trends" rel="noopener noreferrer"&gt;https://www.thordata.com/blog/ai-trends&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Responsible use
&lt;/h2&gt;

&lt;p&gt;Use proxy infrastructure only for lawful, compliant workflows involving permitted public data. Respect applicable laws, website terms, robots directives, rate limits and opt-out requests. Do not use it for credential testing, fake accounts, click manipulation, spam, private-data collection or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Try Thordata: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Reduce Proxy Costs Without Sacrificing Data Quality</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Tue, 15 Sep 2026 09:39:34 +0000</pubDate>
      <link>https://dev.to/thordata-flora/how-to-reduce-proxy-costs-without-sacrificing-data-quality-5bao</link>
      <guid>https://dev.to/thordata-flora/how-to-reduce-proxy-costs-without-sacrificing-data-quality-5bao</guid>
      <description>&lt;h2&gt;
  
  
  How to Reduce Proxy Costs Without Sacrificing Data Quality
&lt;/h2&gt;

&lt;p&gt;The cheapest proxy is not always the lowest-cost option. A failed request can create retry traffic, parser work, manual review and delayed data delivery.&lt;/p&gt;

&lt;p&gt;Start by separating three metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost per request&lt;/li&gt;
&lt;li&gt;Cost per successful response&lt;/li&gt;
&lt;li&gt;Cost per validated usable record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third metric is the one that matters most for a production workflow. Measure it by proxy type, country, session mode and target category.&lt;/p&gt;

&lt;p&gt;Use a bounded test with representative public URLs. Keep the parser and required fields constant, then compare residential, ISP, mobile and datacenter routes where appropriate. Track response time, retry rate, session breaks, missing fields and duplicate records.&lt;/p&gt;

&lt;p&gt;Scale the route with the best usable-record economics, not simply the lowest advertised unit price. If a route starts producing incomplete results, reduce concurrency and review the failure category before increasing traffic.&lt;/p&gt;

&lt;p&gt;Thordata provides residential, ISP, mobile and datacenter proxies for compliant ecommerce research, SEO monitoring, market research and localization testing. Limited free trial access can be used to validate the workflow before scaling.&lt;/p&gt;

&lt;p&gt;Use proxy infrastructure responsibly. Do not use it for private-data collection, fake accounts, spam, click manipulation, credential testing or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Start testing: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Choose a Proxy for a Production Web Data Workflow</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Tue, 15 Sep 2026 06:00:12 +0000</pubDate>
      <link>https://dev.to/thordata-flora/how-to-choose-a-proxy-for-a-production-web-data-workflow-899</link>
      <guid>https://dev.to/thordata-flora/how-to-choose-a-proxy-for-a-production-web-data-workflow-899</guid>
      <description>&lt;h2&gt;
  
  
  How to Choose a Proxy for a Production Web Data Workflow
&lt;/h2&gt;

&lt;p&gt;Choosing a proxy by IP count or headline price is rarely enough. A production workflow should match the proxy type to the target, geography, session model and quality requirements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the workflow
&lt;/h2&gt;

&lt;p&gt;Residential proxies can support geographic diversity and residential network context. ISP proxies can support stable, repeatable sessions. Mobile proxies provide mobile-network context. Datacenter proxies can be useful for fast and controlled workloads when the source and intended use allow them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure usable results
&lt;/h2&gt;

&lt;p&gt;Run a bounded test with representative public URLs. Record location accuracy, response time, session stability, retry rate, data completeness and cost per usable result. A request that returns HTTP 200 but misses the required fields is not a successful business result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a controlled rollout
&lt;/h2&gt;

&lt;p&gt;Start with a small workload, bounded concurrency and limited retries. Increase volume only after the route produces repeatable results. Keep separate failure categories for timeouts, empty results, parser failures and policy restrictions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test before scaling
&lt;/h2&gt;

&lt;p&gt;Thordata offers residential, ISP, mobile and datacenter proxies for compliant web data workflows. A free trial is available for testing; check the current plan and usage terms before starting a production workload.&lt;/p&gt;

&lt;p&gt;Use the trial to compare the proxy type, country and session mode that fit your actual workflow rather than choosing only by the advertised IP count.&lt;/p&gt;

&lt;h2&gt;
  
  
  Responsible use
&lt;/h2&gt;

&lt;p&gt;Use proxy infrastructure for lawful workflows such as public ecommerce research, SEO monitoring, market research, advertising verification and localization testing. Do not use it for credential testing, fake accounts, click manipulation, spam, private-data collection or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Start a Thordata trial: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Mobile proxies: what to measure before scaling</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Tue, 15 Sep 2026 02:00:12 +0000</pubDate>
      <link>https://dev.to/thordata-flora/mobile-proxies-what-to-measure-before-scaling-2c54</link>
      <guid>https://dev.to/thordata-flora/mobile-proxies-what-to-measure-before-scaling-2c54</guid>
      <description>&lt;h2&gt;
  
  
  Mobile proxies: a production evaluation guide
&lt;/h2&gt;

&lt;p&gt;Proxy selection is often reduced to a question about IP volume or advertised speed. That is not enough for a production data workflow. The right proxy type depends on the target geography, session model, request pattern, failure tolerance and the value of each successful record.&lt;/p&gt;

&lt;p&gt;This guide explains how to evaluate the infrastructure before scaling a compliant public-web data project.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Match the proxy type to the workflow
&lt;/h2&gt;

&lt;p&gt;Mobile proxies fit workflows that depend on real 4G/5G network context. Validate location, session behavior and data quality rather than judging by IP count alone.&lt;/p&gt;

&lt;p&gt;Residential proxies are generally useful when geographic diversity and residential network context matter. ISP proxies can be useful for repeatable sessions and predictable connectivity. Mobile proxies are relevant when the workflow needs mobile-network context. Datacenter proxies are often attractive for speed and controlled large-volume workloads.&lt;/p&gt;

&lt;p&gt;The choice should be made from measured results, not from the product name alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Build a controlled test
&lt;/h2&gt;

&lt;p&gt;Use a small, permitted set of public URLs that represents the real workload. Keep the test constant while changing only one variable at a time:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ext&lt;br&gt;
target URLs ? proxy type ? country ? session mode ? request rate ? result validation&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For each request, record the timestamp, target country, proxy type, session identifier, response status, response time, retry count and whether the returned data is complete. Do not store credentials or personal information in the test logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Measure the metrics that affect cost
&lt;/h2&gt;

&lt;p&gt;At minimum, calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Success rate = successful usable results / total attempts&lt;/li&gt;
&lt;li&gt;Median and P95 response time&lt;/li&gt;
&lt;li&gt;Retry rate&lt;/li&gt;
&lt;li&gt;Session break rate&lt;/li&gt;
&lt;li&gt;Geographic accuracy rate&lt;/li&gt;
&lt;li&gt;Cost per usable result&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last metric is usually more useful than cost per gigabyte. A low price is not efficient if failed requests create retries, manual review or incomplete downstream records.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Handle failure explicitly
&lt;/h2&gt;

&lt;p&gt;Use bounded retries with exponential backoff. Stop retrying when the error indicates an invalid request or a policy restriction. Separate transport failures, timeouts, empty results and parsing failures so that one generic retry loop does not hide the real problem.&lt;/p&gt;

&lt;p&gt;A basic workflow should also include a circuit breaker: if the success rate for a location falls below an agreed threshold, pause that route and alert the operator instead of increasing traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Scale gradually
&lt;/h2&gt;

&lt;p&gt;Move from a small test to a limited production run, then compare the same metrics again. Introduce concurrency gradually and watch response time, error rate, bandwidth and downstream parser quality. Keep a rollback path so a target, location or route can be paused without stopping the entire pipeline.&lt;/p&gt;

&lt;p&gt;Today's related industry signal is &lt;strong&gt;reliable proxy infrastructure and practical AI data workflows&lt;/strong&gt;. It is a reminder that data collection infrastructure needs operational measurement as well as a working API.&lt;/p&gt;

&lt;p&gt;Thordata reference: &lt;a href="https://www.thordata.com/blog/ai-trends" rel="noopener noreferrer"&gt;https://www.thordata.com/blog/ai-trends&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Responsible use
&lt;/h2&gt;

&lt;p&gt;Use proxy infrastructure only for lawful, compliant workflows involving permitted public data. Respect applicable laws, website terms, robots directives, rate limits and opt-out requests. Do not use it for credential testing, fake accounts, click manipulation, spam, private-data collection or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Try Thordata: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Reduce Proxy Costs Without Sacrificing Data Quality</title>
      <dc:creator>Flora</dc:creator>
      <pubDate>Mon, 14 Sep 2026 09:00:14 +0000</pubDate>
      <link>https://dev.to/thordata-flora/how-to-reduce-proxy-costs-without-sacrificing-data-quality-4a4j</link>
      <guid>https://dev.to/thordata-flora/how-to-reduce-proxy-costs-without-sacrificing-data-quality-4a4j</guid>
      <description>&lt;h2&gt;
  
  
  How to Reduce Proxy Costs Without Sacrificing Data Quality
&lt;/h2&gt;

&lt;p&gt;The cheapest proxy is not always the lowest-cost option. A failed request can create retry traffic, parser work, manual review and delayed data delivery.&lt;/p&gt;

&lt;p&gt;Start by separating three metrics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cost per request&lt;/li&gt;
&lt;li&gt;Cost per successful response&lt;/li&gt;
&lt;li&gt;Cost per validated usable record&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The third metric is the one that matters most for a production workflow. Measure it by proxy type, country, session mode and target category.&lt;/p&gt;

&lt;p&gt;Use a bounded test with representative public URLs. Keep the parser and required fields constant, then compare residential, ISP, mobile and datacenter routes where appropriate. Track response time, retry rate, session breaks, missing fields and duplicate records.&lt;/p&gt;

&lt;p&gt;Scale the route with the best usable-record economics, not simply the lowest advertised unit price. If a route starts producing incomplete results, reduce concurrency and review the failure category before increasing traffic.&lt;/p&gt;

&lt;p&gt;Thordata provides residential, ISP, mobile and datacenter proxies for compliant ecommerce research, SEO monitoring, market research and localization testing. Limited free trial access can be used to validate the workflow before scaling.&lt;/p&gt;

&lt;p&gt;Use proxy infrastructure responsibly. Do not use it for private-data collection, fake accounts, spam, click manipulation, credential testing or attempts to evade platform enforcement.&lt;/p&gt;

&lt;p&gt;Start testing: &lt;a href="https://www.thordata.com/?ls=dev&amp;amp;lk=DEV" rel="noopener noreferrer"&gt;https://www.thordata.com/?ls=dev&amp;amp;lk=DEV&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
