<?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: Lucas</title>
    <description>The latest articles on DEV Community by Lucas (@ipvolt).</description>
    <link>https://dev.to/ipvolt</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%2F4141024%2F8e3c92f5-a73e-405c-be65-550c8785ad87.png</url>
      <title>DEV Community: Lucas</title>
      <link>https://dev.to/ipvolt</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ipvolt"/>
    <language>en</language>
    <item>
      <title>I measured how many pages fit in 1 GB of proxy traffic</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Wed, 07 Oct 2026 10:01:58 +0000</pubDate>
      <link>https://dev.to/ipvolt/i-measured-how-many-pages-fit-in-1-gb-of-proxy-traffic-1o9o</link>
      <guid>https://dev.to/ipvolt/i-measured-how-many-pages-fit-in-1-gb-of-proxy-traffic-1o9o</guid>
      <description>&lt;p&gt;Proxy plans are sold per GB, but scrapers think in pages. So when someone asks "how many GB do I need?", the usual answer is "it depends". That's true, and it doesn't help anyone pick a plan.&lt;/p&gt;

&lt;p&gt;So I measured it. 34 public pages, fetched three ways, with every byte counted at the tunnel in both directions.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short answer
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;How you fetch&lt;/th&gt;
&lt;th&gt;Median per page&lt;/th&gt;
&lt;th&gt;Pages per GB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;HTTP client, HTML only&lt;/td&gt;
&lt;td&gt;22.7 KB&lt;/td&gt;
&lt;td&gt;44,083&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright, full load&lt;/td&gt;
&lt;td&gt;1.10 MB&lt;/td&gt;
&lt;td&gt;906&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Playwright, images/media/fonts blocked&lt;/td&gt;
&lt;td&gt;454.6 KB&lt;/td&gt;
&lt;td&gt;2,199&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Same pages, same day. At the median, the full browser load used about 49 times the traffic of a plain HTML fetch.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I measured
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;34 public pages: docs, news articles, blog posts, forum threads, shop category and product pages, and a few JavaScript apps. Only pages robots.txt allows, nothing behind a login.&lt;/li&gt;
&lt;li&gt;Three modes: Requests 2.34.2 (one GET, compression on), Playwright 1.63.0 full load, and Playwright with images, media and fonts blocked.&lt;/li&gt;
&lt;li&gt;Three runs per page, fresh process and empty cache every time. 306 loads, all HTTP 200.&lt;/li&gt;
&lt;li&gt;All traffic went through scrapescope, a small local proxy I built that counts bytes per tunnel. So the numbers include TLS, headers and upload, not just page weight.&lt;/li&gt;
&lt;li&gt;October 5, 2026, from Helsinki, direct connection.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Three things that surprised me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Scripts, not images.&lt;/strong&gt; In full loads, scripts were 49% of the bytes. Images, media and fonts together were 35%. Blocking them saved 28% at the median, less than I expected. On news pages the blocked loads were actually heavier (2.24 MB vs 1.32 MB median), because pages behave differently when things fail to load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Half the bytes go to other companies.&lt;/strong&gt; 49.9% of full-load bytes went to third-party hosts. Google Tag Manager alone was on 17 of the 34 pages at about 308 KB per load. That's 12% of all full-load bytes, for analytics your scraper will never use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Compression does a lot of quiet work.&lt;/strong&gt; All 34 HTML responses were compressed (17 Brotli, 14 gzip, 3 zstd). The median HTML document was 83.5 KB decoded but 22.7 KB on the wire. A client that doesn't ask for compression pays for the bigger number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Upload counts too
&lt;/h2&gt;

&lt;p&gt;Upstream was 6.3% of the traffic for HTML-only fetches and about 3% for browser loads. Small, but if your provider bills both directions, it's on your bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Estimate your own
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GB per day = pages per day × bytes per page × attempts per page ÷ 1,000,000,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;50,000 pages a day with a full browser (1.10 MB median, 1.2 attempts per page) comes to about 66 GB a day. The same job with an HTTP client is about 1.4 GB.&lt;/p&gt;

&lt;p&gt;Your pages won't match mine, so treat these as a starting point. Better still, run your own job through scrapescope for a day and use your numbers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;One location, one day, 34 pages. Direct connection, so no proxy exit location, blocks or retries. CONNECT bytes are estimated. Pages change.&lt;/p&gt;

&lt;p&gt;The full write-up has the breakdown by page type, cost per 1,000 pages at different per-GB prices, the raw CSV of all 306 runs and the scripts: &lt;a href="https://ipvolt.com/blog/how-many-gb-of-proxies-do-i-need" rel="noopener noreferrer"&gt;How many GB of proxies do you need?&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;scrapescope is open source: &lt;a href="https://github.com/ipvolt/scrapescope" rel="noopener noreferrer"&gt;github.com/ipvolt/scrapescope&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I'm building ipvolt, a proxy service that bills per GB, which is why I wanted real numbers. scrapescope works with any provider.&lt;/p&gt;

&lt;p&gt;What do your scrapers average per page? I'm curious whether anyone sees numbers very different from these.&lt;/p&gt;

</description>
      <category>webscraping</category>
      <category>python</category>
      <category>playwright</category>
      <category>performance</category>
    </item>
    <item>
      <title>My coding agent kept "fixing" proxy errors wrong, so I built an MCP server</title>
      <dc:creator>Lucas</dc:creator>
      <pubDate>Fri, 02 Oct 2026 13:08:43 +0000</pubDate>
      <link>https://dev.to/ipvolt/my-coding-agent-kept-fixing-proxy-errors-wrong-so-i-built-an-mcp-server-3jg3</link>
      <guid>https://dev.to/ipvolt/my-coding-agent-kept-fixing-proxy-errors-wrong-so-i-built-an-mcp-server-3jg3</guid>
      <description>&lt;p&gt;I work on proxy infrastructure, which means I read a lot of proxy errors. And I kept noticing the same thing: the coding agents I used were great at almost everything except this.  &lt;/p&gt;

&lt;p&gt;Give one a 407 and it would happily "fix" it by putting the proxy credentials into the target site's &lt;code&gt;Authorization&lt;/code&gt; header. Show it a timeout on a POST and it would just retry, without asking whether the first request had already reached the server.&lt;/p&gt;

&lt;p&gt;That's not the model being dumb. Proxy errors are a narrow area with a lot of confidently wrong answers floating around the internet, and the model has read plenty of them.&lt;/p&gt;

&lt;p&gt;So I built a small MCP server that gives the agent grounded answers for this one area. It's free, open source and works with any proxy provider. Here's what I learned designing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't let the agent paste raw logs
&lt;/h2&gt;

&lt;p&gt;The diagnose tool doesn't accept logs, URLs or credentials. It takes structured observations instead. This is a real input for the 407 case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"client"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"requests"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2.34.2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"phase"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"connect_tunnel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;407&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"responseSource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"proxy"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two reasons. Logs are full of things that shouldn't end up in a model's context, like credentials and session cookies. And the phase matters more than people expect. A 407 during the CONNECT tunnel is your proxy talking, not the website, so the fix is in your proxy config and never in the target's headers.&lt;/p&gt;

&lt;p&gt;The schema is strict on purpose. Unknown fields are rejected, and &lt;code&gt;responseSource&lt;/code&gt; should only say &lt;code&gt;proxy&lt;/code&gt; or &lt;code&gt;target&lt;/code&gt; when you actually know which layer answered, not because of the status code alone.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Never recommend an automatic retry
&lt;/h2&gt;

&lt;p&gt;Every diagnosis comes back with likely causes, the next thing to check, and what it can't know. For the 407 above, the summary starts with this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;407 reports an intermediary authentication requirement. It does not identify a wrong password as the sole cause.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Retries got the strictest treatment. In the output schema, &lt;code&gt;automaticRetryRecommended&lt;/code&gt; isn't a boolean. It's the literal &lt;code&gt;false&lt;/code&gt;. The tool can only tell the agent what kind of retry thinking applies. Here's what comes back for a POST that timed out waiting for response headers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"retry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"automaticRetryRecommended"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"reconcile_before_repeating"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"advice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Do not automatically repeat POST/PATCH or an operation of unknown semantics. ..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a write timed out after it left your machine, the order might already exist. A missing response isn't proof that nothing happened, and the tool says so instead of letting the agent assume.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Credentials never go through the agent
&lt;/h2&gt;

&lt;p&gt;Config templates use environment variable placeholders. No tool asks for a password as an argument, and no response ever contains one. If the agent never sees the secret, it can't paste it into a transcript, a log or a commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The one tool that touches the network is boring on purpose
&lt;/h2&gt;

&lt;p&gt;Run locally, the server sends no telemetry and makes no network requests. There's one optional route check, and it stays off until you set &lt;code&gt;IPVOLT_ENABLE_ROUTE_CHECK=1&lt;/code&gt;. Then it sends a single request through your proxy to one fixed endpoint, with a 10-second deadline, and that's all it can do. It can't be pointed at an arbitrary URL, so it can't be turned into a port scanner.&lt;/p&gt;

&lt;p&gt;The hosted version keeps basic metrics: tool name, success or error, duration and toolkit version. Nothing about your requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Return structured data and text
&lt;/h2&gt;

&lt;p&gt;Every tool has an output schema, and responses include both the structured result and the same result as text. Clients that read structured content get clean JSON they can pipe into the next step. Clients that don't still get a usable answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trade-offs
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client versions are pinned.&lt;/strong&gt; The diagnose tool only accepts the exact versions the rules were tested against: curl 8.22.0, Requests 2.34.2, HTTPX 0.28.1 and Playwright 1.63.0. That keeps the answers honest, but anyone on another version gets an error instead of a diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The local package requires Node 24+.&lt;/strong&gt; That rules out everyone still on Node 22.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first npm release went out without build provenance.&lt;/strong&gt; The next one should come from CI with provenance attached.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Use the hosted endpoint (&lt;code&gt;https://mcp.ipvolt.com/mcp&lt;/code&gt;, Streamable HTTP, no signup or API key), or run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx &lt;span class="nt"&gt;--yes&lt;/span&gt; @ipvolt/proxy-toolkit-mcp@0.1.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Setup for different MCP clients is at &lt;a href="https://ipvolt.com/mcp" rel="noopener noreferrer"&gt;ipvolt.com/mcp&lt;/a&gt;, and the code is on &lt;a href="https://github.com/ipvolt/proxy-toolkit-mcp" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Full disclosure: I'm building ipvolt, a proxy service. The toolkit is provider-neutral and works fine without it.&lt;/p&gt;

&lt;p&gt;If you've built MCP tools yourself: do you return structured data, text, or both? Curious what's worked for you.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>webdev</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
