<?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: Maxi</title>
    <description>The latest articles on DEV Community by Maxi (@taventix).</description>
    <link>https://dev.to/taventix</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%2F4144081%2F3b350af6-fd45-429a-925e-635f7945756d.jpg</url>
      <title>DEV Community: Maxi</title>
      <link>https://dev.to/taventix</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/taventix"/>
    <language>en</language>
    <item>
      <title>3.6 million checks of free proxy lists in a day: what the numbers show, and what they don't</title>
      <dc:creator>Maxi</dc:creator>
      <pubDate>Fri, 16 Oct 2026 21:01:57 +0000</pubDate>
      <link>https://dev.to/taventix/36-million-checks-of-free-proxy-lists-in-a-day-what-the-numbers-show-and-what-they-dont-1jfd</link>
      <guid>https://dev.to/taventix/36-million-checks-of-free-proxy-lists-in-a-day-what-the-numbers-show-and-what-they-dont-1jfd</guid>
      <description>&lt;p&gt;Free proxy lists have a reputation: most entries are dead, and the ones that answer are slow, blocked or worse. I wanted numbers instead of a reputation, so &lt;a href="https://github.com/maximilianfeix/proxy-scraper" rel="noopener noreferrer"&gt;proxy-scraper&lt;/a&gt; checks the public lists about every hour in GitHub Actions and publishes what passes.&lt;/p&gt;

&lt;p&gt;This post has the numbers from 24 of those runs (28 Sep 09:43 UTC to 29 Sep 08:39 UTC, 2026) and from a week of published lists. An earlier version of it drew conclusions the data doesn't carry. This one says for every number what was counted, out of what, and what it can't tell you. Every number in the two main sections comes out of a script in &lt;a href="https://github.com/maximilianfeix/proxy-scraper/tree/main/docs/analysis" rel="noopener noreferrer"&gt;&lt;code&gt;docs/analysis/&lt;/code&gt;&lt;/a&gt; that you can run yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The funnel
&lt;/h2&gt;

&lt;p&gt;Each run reads about 800 public proxy lists and ends up with &lt;strong&gt;1.26 million candidates&lt;/strong&gt; on average. A candidate is a protocol plus an address: the same &lt;code&gt;ip:port&lt;/code&gt; listed as HTTP and as SOCKS5 counts twice. The run then checks the 150,000 most promising ones. Proxies that passed before and lists with a good hit rate go first.&lt;/p&gt;

&lt;p&gt;24 runs × 150,000 = &lt;strong&gt;3.6 million checks&lt;/strong&gt;. That is not 3.6 million proxies. The same sources go first in every run, so the 24 samples overlap, probably heavily. How many distinct candidates were checked that day was never recorded.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What has to happen&lt;/th&gt;
&lt;th&gt;Over 24 runs&lt;/th&gt;
&lt;th&gt;Share&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;First request&lt;/td&gt;
&lt;td&gt;the proxy speaks HTTP, SOCKS4 or SOCKS5 and returns a public exit IP from a check target&lt;/td&gt;
&lt;td&gt;93,244 answered&lt;/td&gt;
&lt;td&gt;2.6 % of checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Second request&lt;/td&gt;
&lt;td&gt;a request to a second site (httpbin.org) returns valid JSON with the same exit IP&lt;/td&gt;
&lt;td&gt;50,193 did not pass&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;54 % of those that answered&lt;/strong&gt; (41–67 % per run)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content&lt;/td&gt;
&lt;td&gt;a static test page arrives byte for byte&lt;/td&gt;
&lt;td&gt;2,244 arrived with a different hash&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;at least 5.2 %&lt;/strong&gt;: 2,244 of the 43,051 that got this far&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kept&lt;/td&gt;
&lt;td&gt;the first two passed and the test page was not seen to differ&lt;/td&gt;
&lt;td&gt;40,807&lt;/td&gt;
&lt;td&gt;1.1 % of checks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python docs/analysis/funnel_summary.py docs/analysis/data/runs-2026-09-28_29.csv 2026-09-28T09:40 2026-09-29T08:45
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The per-run numbers in that CSV were read from the workflow's console logs (&lt;a href="https://github.com/maximilianfeix/proxy-scraper/blob/main/docs/analysis/funnel_from_logs.py" rel="noopener noreferrer"&gt;&lt;code&gt;funnel_from_logs.py&lt;/code&gt;&lt;/a&gt;). GitHub deletes those logs after 90 days, so the CSV is the copy that stays.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the funnel doesn't tell you
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Why the second request failed.&lt;/strong&gt; The logs have a count and no reasons. A proxy fails this step if it can't load two pages in a row, if its exit IP changes between the two requests (a rotating or chained exit), if it only answers the first check, or if the second site turns it away. The earlier version of this page called these "honeypots". Some may be. Nothing that was measured says how many.&lt;/p&gt;

&lt;p&gt;It also counts httpbin.org's own bad minutes. Until 1.30 the tool asked httpbin.org directly once, at the start of a run, and never again. If it went down halfway, every proxy after that failed the second request. None of the 24 runs logged a missing second target at the start; what happened during them is not in the logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why the page differed.&lt;/strong&gt; The check compares one hash. A page cut off by the proxy, a cached older copy, a block page with status 200 and an injected script all give a different hash. The earlier version said these proxies "usually inject a script". That came from one early look at 270 proxies, of which 54 returned a changed page. No sample of those pages was kept, so it is not a finding. The earlier version also gave the share as "1 in 40 working proxies", which used the wrong denominator: 2,244 is 2.4 % of everything that answered, and 5.2 % of the proxies that reached this check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many pages were compared.&lt;/strong&gt; In these runs a content request that timed out or came back as an error page counted as "unchanged", and the proxy was kept. So the 40,807 include an unknown number of proxies whose test page never arrived, and 5.2 % is a lower bound for the share of compared pages that differed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How many free proxies work.&lt;/strong&gt; 1.1 % is the share of a prioritised sample. Known-good proxies go first, so a random entry of these lists does worse. The checks ran with a 3 s connect timeout and 6 s overall, from GitHub-hosted runners. A proxy that fails from there can work from somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a "not checked" proxy looks like.&lt;/strong&gt; Of 1.26 million candidates per run, 1.11 million were never checked. The runs recorded nothing about them. They are not "dead", they are unknown.&lt;/p&gt;

&lt;h2&gt;
  
  
  How long entries stay on the list
&lt;/h2&gt;

&lt;p&gt;Every published run notes which entries are on the list. &lt;code&gt;seen.json&lt;/code&gt; keeps that as one bit per entry and run for the last week. The numbers below are from the week ending 13 Oct 2026, 16:47 UTC (156 published runs).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python docs/analysis/list_churn.py list/        # see the top of the script for how to get the three files
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First, what an entry is. The week saw 41,634 different &lt;code&gt;type://ip:port&lt;/code&gt; entries. They sit on 4,890 hosts. &lt;strong&gt;82 % of all entries are ports on 16 hosts&lt;/strong&gt;, each with 50 or more entries ("crowded hosts" below). The list on 13 Oct had 3,074 entries on 527 hosts with 618 different exit IPs. So "3,000 proxies" is closer to 500 machines, and every statistic about entries is mostly a statistic about a few hosts with hundreds of ports whose entries come and go from the list.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;per entry&lt;/th&gt;
&lt;th&gt;per host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;On the list at least once&lt;/td&gt;
&lt;td&gt;41,634&lt;/td&gt;
&lt;td&gt;4,890&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Off the list at the end of the week&lt;/td&gt;
&lt;td&gt;38,398&lt;/td&gt;
&lt;td&gt;4,333&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… whose last stretch on the list was a single run&lt;/td&gt;
&lt;td&gt;32,819 (85 %)&lt;/td&gt;
&lt;td&gt;3,666 (85 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… whose last stretch was 24 runs or more&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Every stretch that began and ended inside the week&lt;/td&gt;
&lt;td&gt;142,517&lt;/td&gt;
&lt;td&gt;36,034&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… one run long&lt;/td&gt;
&lt;td&gt;111,059 (78 %)&lt;/td&gt;
&lt;td&gt;26,742 (74 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… 24 runs or more&lt;/td&gt;
&lt;td&gt;215&lt;/td&gt;
&lt;td&gt;203&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Still listed, for 24 or more runs so far&lt;/td&gt;
&lt;td&gt;57&lt;/td&gt;
&lt;td&gt;71&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The earlier version of this page said of the entries that had dropped off the list: "85 % were gone by the very next check, an hour later", and concluded that a day-old list is mostly dead. The 85 % is real, but it is narrower than it sounds: it counts only entries that are off the list at the end, and only the last stretch of each. Counting every stretch, 78 % were one run long, and 215 lasted a day or more, not 12. Entries that stay are in neither number. Asked the other way round:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Of the entries on one run's list …&lt;/th&gt;
&lt;th&gt;all entries&lt;/th&gt;
&lt;th&gt;without the 16 crowded hosts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;… on the list again one run later&lt;/td&gt;
&lt;td&gt;40 % (16–58 %)&lt;/td&gt;
&lt;td&gt;57 % (20–84 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;… on the list again 24 runs later&lt;/td&gt;
&lt;td&gt;26 % (8–40 %)&lt;/td&gt;
&lt;td&gt;49 % (17–69 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Median over the week's runs, range in brackets. So about a quarter of a list is on the list again a day later, and about half once the 16 crowded hosts are left out. The earlier sentence "a list that is a day old is mostly a list of dead proxies" doesn't hold for the second group.&lt;/p&gt;

&lt;h3&gt;
  
  
  What this doesn't tell you
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;That an entry failed.&lt;/strong&gt; Not being on the list is not a recorded failure. An entry is missing from a run if it failed a check, but also if the run didn't get to it, if the runner was slow that hour, or if the second check target had a bad moment. Before 1.30 the published data can't tell these apart.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How long a proxy lives.&lt;/strong&gt; Runs are about hourly: the median gap was 0.96 h, eight gaps were over 1.5 h and one was 13.9 h. "24 runs" is roughly a day, not exactly. Stretches that began before the week or haven't ended are left out of the "left the list" rows, and those are the long ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  One list, looked at closely
&lt;/h2&gt;

&lt;p&gt;These are single snapshots. They are here because they were in the first version, with what they are worth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Big sites.&lt;/strong&gt; After each run the HTTPS-capable entries send one request each to a few big sites. The table shows the share that got a normal page, out of those that gave a clear answer, for three stored lists:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Site&lt;/th&gt;
&lt;th&gt;29 Sep, 01:14&lt;/th&gt;
&lt;th&gt;13 Oct, 01:23&lt;/th&gt;
&lt;th&gt;13 Oct, 16:47&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Google&lt;/td&gt;
&lt;td&gt;186 of 776 (24 %)&lt;/td&gt;
&lt;td&gt;594 of 716 (83 %)&lt;/td&gt;
&lt;td&gt;611 of 916 (67 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;TikTok&lt;/td&gt;
&lt;td&gt;401 of 734 (55 %)&lt;/td&gt;
&lt;td&gt;394 of 628 (63 %)&lt;/td&gt;
&lt;td&gt;652 of 813 (80 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reddit&lt;/td&gt;
&lt;td&gt;345 of 766 (45 %)&lt;/td&gt;
&lt;td&gt;214 of 572 (37 %)&lt;/td&gt;
&lt;td&gt;243 of 867 (28 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instagram&lt;/td&gt;
&lt;td&gt;323 of 801 (40 %)&lt;/td&gt;
&lt;td&gt;106 of 805 (13 %)&lt;/td&gt;
&lt;td&gt;153 of 1,103 (14 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon&lt;/td&gt;
&lt;td&gt;242 of 856 (28 %)&lt;/td&gt;
&lt;td&gt;22 of 795 (3 %)&lt;/td&gt;
&lt;td&gt;226 of 1,169 (19 %)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first version showed one run (Google 47 %, Amazon 2 %) and concluded "Amazon blocks almost everything". The three columns above differ by a factor of up to ten for the same site, so one run is not a ranking of sites. It mostly reflects which gateways were on the list that hour. The split into "datacenter" and "other" exits in the first version rested on whether a provider's name looks like a hosting company. "Other" is everything else. It was never shown to be home or mobile lines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the entries on a list look like&lt;/strong&gt; (list of 29 Sep, 08:49 UTC, 2,914 entries, from the run's log and &lt;code&gt;history.json&lt;/code&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;62 % have a provider name that looks like a hosting company; in the three lists above it was 48 %, 62 % and 65 %&lt;/li&gt;
&lt;li&gt;28 % exit from an IP on the SpamCop blocklist; 43 %, 36 % and 34 % in the three lists above&lt;/li&gt;
&lt;li&gt;64 % passed the HTTPS test: a tunnel to one test host with a certificate that verifies, at check time&lt;/li&gt;
&lt;li&gt;median first answer after 1.9 s; median download speed 68 KiB/s among the HTTPS-capable entries from which at least 20 KB of a 100 KB download arrived within 12 seconds&lt;/li&gt;
&lt;li&gt;most entries were SOCKS5, and most SOCKS5 entries are ports on the crowded hosts (937 of 1,122 in that day's stored list, the 01:14 snapshot, all in one /24), so this says little about the protocols&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What changes with 1.30
&lt;/h2&gt;

&lt;p&gt;The gaps above are gaps in what was recorded. From 1.30 on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;every check of every kept proxy ends as &lt;code&gt;passed&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;unavailable&lt;/code&gt; or &lt;code&gt;skipped&lt;/code&gt;, with a reason code, the target and the time (&lt;a href="https://github.com/maximilianfeix/proxy-scraper/blob/main/docs/check-states.md" rel="noopener noreferrer"&gt;check states&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;a check target that goes down mid-run no longer fails the proxies checked against it; they are marked &lt;code&gt;unavailable&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;every run writes &lt;code&gt;run.json&lt;/code&gt;: candidates, checked, not checked, and each check by state and reason. The hourly list publishes it and keeps the counts in &lt;code&gt;history.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;run.json&lt;/code&gt; counts how many of the changed test pages contain a &lt;code&gt;&amp;lt;script&lt;/code&gt; tag and how many don't (the reference page has none), and &lt;code&gt;--diagnose&lt;/code&gt; shows size and hash of the page one proxy returned. That is still not a stored sample, but it is the first thing that could support or refute "scripts"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A follow-up with a few weeks of that data can answer what this page can't: how the second request fails, and how often the page differs for a reason that matters.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmaximilianfeix%2Fproxy-scraper%2Fmain%2Fdocs%2Fdemo%2Fchecks.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fmaximilianfeix%2Fproxy-scraper%2Fmain%2Fdocs%2Fdemo%2Fchecks.gif" alt="Terminal recording: proxy-scraper --pick 2 --verify prints two proxies and says why the other four were left out; --diagnose shows each check of one proxy" width="800" height="320"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The recording runs against six simulated proxies on localhost. It shows how results are reported and measures nothing about public proxies.&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The current list with filters: &lt;a href="https://maximilianfeix.github.io/proxy-scraper/" rel="noopener noreferrer"&gt;https://maximilianfeix.github.io/proxy-scraper/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The lists alone, per protocol and country: &lt;a href="https://github.com/maximilianfeix/free-proxy-list" rel="noopener noreferrer"&gt;https://github.com/maximilianfeix/free-proxy-list&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Check one proxy and see what each check found: &lt;code&gt;uvx proxy-scraper-cli --diagnose socks5://HOST:PORT&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Source, MIT licensed: &lt;a href="https://github.com/maximilianfeix/proxy-scraper" rel="noopener noreferrer"&gt;https://github.com/maximilianfeix/proxy-scraper&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Free proxies are run by strangers. Never send passwords, cookies or personal data through them.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this post was drafted with an AI assistant from the repository's own analysis scripts and data; every number comes from the scripts linked above. The canonical version lives in the repository.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>webscraping</category>
      <category>opensource</category>
      <category>showdev</category>
    </item>
    <item>
      <title>What changed in this Electron update? Diffing two app.asar builds without running either</title>
      <dc:creator>Maxi</dc:creator>
      <pubDate>Fri, 16 Oct 2026 20:52:39 +0000</pubDate>
      <link>https://dev.to/taventix/what-changed-in-this-electron-update-diffing-two-appasar-builds-without-running-either-422d</link>
      <guid>https://dev.to/taventix/what-changed-in-this-electron-update-diffing-two-appasar-builds-without-running-either-422d</guid>
      <description>&lt;p&gt;Your Electron app's 1.5.0 is ready. Can a web page in it now reach something it couldn't in 1.4.0? Did a window lose its sandbox? Does the code call a server it didn't call before?&lt;/p&gt;

&lt;p&gt;The diff of two minified &lt;code&gt;app.asar&lt;/code&gt; bundles won't tell you. Running the new build to find out is exactly what you wanted to avoid. So I built a tool that reads both builds statically and tells you what changed, with the file and line behind every change, and an honest list of what it &lt;em&gt;couldn't&lt;/em&gt; figure out.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3dz551yz6uxjjwqymv26.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3dz551yz6uxjjwqymv26.png" alt="hexvero comparing two builds of an Electron app" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Everything below is reproducible in about ten seconds. You need Node 22+.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Two builds, one command
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hexvero demo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This copies two builds of a small example Electron app (NotesDesk 1.4.0 and 1.5.0, written for these examples, nothing from a real product) into &lt;code&gt;./hexvero-demo&lt;/code&gt; and compares them.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What a page can reach
&lt;/h2&gt;

&lt;p&gt;In Electron, a page talks to the main process through the preload script: &lt;code&gt;contextBridge&lt;/code&gt; exposes members, each sends on an IPC channel, and &lt;code&gt;ipcMain.handle&lt;/code&gt; answers. hexvero joins the three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interfaces (2)
  + preload.js::notesdesk::checkUpdate  @ b: preload.js:9, handler main.js:30 · link direct
  - preload.js::notesdesk::openInShell  @ a: preload.js:9, handler main.js:30 · link direct
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1.4.0 let any page open a path in the system shell through &lt;code&gt;openInShell&lt;/code&gt;. 1.5.0 removes it and adds &lt;code&gt;checkUpdate&lt;/code&gt;, whose handler at &lt;code&gt;main.js:30&lt;/code&gt; fetches an update URL. &lt;code&gt;link direct&lt;/code&gt; means the bridge member and the handler use the same literal channel, so the link isn't a guess.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What a window may do
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;windows (1)
  ~ main.js::win  @ b: main.js:8
      nodeIntegration: true → false, nodeIntegrationInEffect: "on" → "off", sandbox: false → true
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Settings are reported &lt;strong&gt;as written&lt;/strong&gt;. If a value isn't written and rests on Electron's default for the version in &lt;code&gt;package.json&lt;/code&gt;, it says &lt;code&gt;default on&lt;/code&gt; / &lt;code&gt;default off&lt;/code&gt; instead of pretending it read it.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. What it could &lt;em&gt;not&lt;/em&gt; tell
&lt;/h2&gt;

&lt;p&gt;This is the part I care about most:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Not established
  ! b: 1 IPC call(s) pass a channel that is not a literal or a constant that could be followed
    (a parameter, a computed value). … ipcMain.handle at main.js:33. (ipc_channel_unresolved)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;1.5.0 registers &lt;code&gt;plugin:${name}&lt;/code&gt; channels with names from an environment variable. No static reading can list them. So instead of guessing (or silently dropping them), hexvero names the call and the line, and the IPC finding turns from "confirmed" to "inferred". Same in comparisons: an item in a file the other build didn't read is &lt;code&gt;unknown&lt;/code&gt;, never &lt;code&gt;removed&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Check it, write it down
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hexvero ui &lt;span class="nt"&gt;--demo&lt;/span&gt; &lt;span class="nt"&gt;--open&lt;/span&gt;
npx hexvero compare hexvero-demo/notesdesk-1.4.0.asar hexvero-demo/notesdesk-1.5.0.asar &lt;span class="nt"&gt;--report&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The local web UI (127.0.0.1, per-session token, nothing loaded from the network) opens the source lines behind every &lt;code&gt;file:line&lt;/code&gt;. The report is plain Markdown you can attach to a release review; same inputs, same bytes. &lt;a href="https://maximilianfeix.github.io/hexvero/demo/" rel="noopener noreferrer"&gt;Here's the example report.&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fact4sziq66y1ky79ml85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fact4sziq66y1ky79ml85.png" alt="source lines around main.js:30" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do (honestly)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It reads files; it never runs the app. A finding is what the code &lt;strong&gt;contains&lt;/strong&gt;, not what it does, and not proof that anything is exploitable.&lt;/li&gt;
&lt;li&gt;It misses things. On my newest blind test set (an app written by someone without access to hexvero, measured once) it found &lt;strong&gt;12 of 57&lt;/strong&gt; IPC channels and &lt;strong&gt;5 of 8&lt;/strong&gt; version changes, mostly because channels came from a registry function it doesn't evaluate. The misses are &lt;a href="https://github.com/maximilianfeix/hexvero/blob/main/corpus/holdout4/README.md" rel="noopener noreferrer"&gt;published in the repo&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;"Doesn't execute the target" isn't a safety guarantee: parsing hostile bytes is still parsing hostile bytes (bounded in size, depth and time).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  On your own app
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx hexvero compare old/MyApp.app/Contents/Resources/app.asar &lt;span class="se"&gt;\&lt;/span&gt;
                    new/MyApp.app/Contents/Resources/app.asar &lt;span class="nt"&gt;--report&lt;/span&gt; report.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It also reads ELF/PE/Mach-O, APKs, .NET, WebAssembly and more, and ships an MCP server so a coding agent (Claude Code, Cursor, Codex…) can run the same investigation with small, cited answers.&lt;/p&gt;

&lt;p&gt;It's MIT and open source: &lt;strong&gt;&lt;a href="https://github.com/maximilianfeix/hexvero" rel="noopener noreferrer"&gt;github.com/maximilianfeix/hexvero&lt;/a&gt;&lt;/strong&gt;. If it gets a fact wrong on your app, an issue with the file and line is the most useful thing you can send me. And if it showed you something you didn't know about your build, a star helps others find it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I'm the author of hexvero. This post was written by an AI agent working on my behalf; every output in it was produced by running the tool. Only use it on software you own or are allowed to examine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>electron</category>
      <category>security</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>docker builder prune freed 0 B: two places Docker keeps disk space that no prune reaches</title>
      <dc:creator>Maxi</dc:creator>
      <pubDate>Thu, 15 Oct 2026 05:33:22 +0000</pubDate>
      <link>https://dev.to/taventix/docker-builder-prune-freed-0-b-two-places-docker-keeps-disk-space-that-no-prune-reaches-3lpb</link>
      <guid>https://dev.to/taventix/docker-builder-prune-freed-0-b-two-places-docker-keeps-disk-space-that-no-prune-reaches-3lpb</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: this article was drafted with AI assistance (Claude) from measurements recorded in the dockdu repository. Every number comes from &lt;a href="https://github.com/maximilianfeix/dockdu/blob/main/docs/verification/buildx-overlay2-2026-10-15.md" rel="noopener noreferrer"&gt;the measurement log&lt;/a&gt; and can be reproduced on a disposable &lt;code&gt;docker:dind&lt;/code&gt; container.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker system df&lt;/code&gt; says the build cache is empty. &lt;code&gt;docker system prune -af&lt;/code&gt; reports &lt;code&gt;Total reclaimed space: 0B&lt;/code&gt;. &lt;code&gt;df -h&lt;/code&gt; still shows the disk filling up. Here are two cases where all three statements are true at once, both measured on Docker Engine 29.9.0, with the commands to check each by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The build cache of a buildx builder is a volume
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker buildx create --driver docker-container&lt;/code&gt; starts BuildKit in its own container, &lt;code&gt;buildx_buildkit_&amp;lt;node&amp;gt;&lt;/code&gt;. The builder keeps its whole state, including its build cache, in a volume named &lt;code&gt;buildx_buildkit_&amp;lt;node&amp;gt;_state&lt;/code&gt; (&lt;a href="https://github.com/docker/buildx/blob/master/driver/docker-container/driver.go" rel="noopener noreferrer"&gt;buildx source&lt;/a&gt;). The node is usually the builder name plus &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured:&lt;/strong&gt; one builder &lt;code&gt;ci&lt;/code&gt;, one build that added 30 MB.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          2         1         430.9MB   73.34MB (17%)
Containers      1         1         40.96kB   0B (0%)
Local Volumes   2         1         73.31MB   114.7kB (0%)
Build Cache     0         0         0B        0B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The 73 MB of build cache is in the &lt;strong&gt;Local Volumes&lt;/strong&gt; row. Then the two prune commands, with &lt;code&gt;du -sk&lt;/code&gt; on the state volume after each:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;State volume on disk&lt;/th&gt;
&lt;th&gt;Prune printed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;(before)&lt;/td&gt;
&lt;td&gt;72452 KiB&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker builder prune -af&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;72452 KiB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Total: 0B&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker buildx prune --builder=ci -af&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;740 KiB&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Total: 73.36MB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;docker builder prune&lt;/code&gt; works on the &lt;em&gt;selected&lt;/em&gt; builder, which here was the daemon's own. A CI host that once ran &lt;code&gt;docker buildx create --use&lt;/code&gt; and was later switched back keeps growing in a volume that &lt;code&gt;docker builder prune&lt;/code&gt; never touches.&lt;/p&gt;

&lt;p&gt;There is one more trap. &lt;code&gt;docker buildx rm --keep-state&lt;/code&gt; removes the builder but keeps the volume. Nothing then refers to it, so it only shows up as an unused volume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;buildx_buildkit_      &lt;span class="c"&gt;# one per docker-container node&lt;/span&gt;
docker system &lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;buildx_buildkit_          &lt;span class="c"&gt;# sizes&lt;/span&gt;
docker buildx &lt;span class="nb"&gt;ls&lt;/span&gt;                                     &lt;span class="c"&gt;# which builder each node belongs to&lt;/span&gt;
docker buildx &lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;--builder&lt;/span&gt; &amp;lt;name&amp;gt;                    &lt;span class="c"&gt;# that builder's cache, record by record&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Free it:&lt;/strong&gt; &lt;code&gt;docker buildx prune --builder &amp;lt;name&amp;gt;&lt;/code&gt;. Add &lt;code&gt;--keep-storage 10gb&lt;/code&gt; to keep a working set. Or set a GC policy in that builder's &lt;code&gt;buildkitd.toml&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Switching to the containerd image store hides your old images
&lt;/h2&gt;

&lt;p&gt;Docker Desktop and fresh Engine 29 installs use the containerd image store. If a daemon is switched to it, the images and containers of the classic overlay2 store are not deleted. Docker's docs say they become hidden: &lt;a href="https://docs.docker.com/engine/storage/containerd/" rel="noopener noreferrer"&gt;"Your overlay2 data remains on disk but becomes hidden."&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured:&lt;/strong&gt; one data root with &lt;code&gt;alpine:3.22&lt;/code&gt; and &lt;code&gt;nginx:1.29-alpine&lt;/code&gt; pulled under the graph driver, then the same data root started with &lt;code&gt;--feature containerd-snapshotter=true&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker images                 # (empty)
$ docker system df              # every row 0B
$ docker system prune -af
Total reclaimed space: 0B
$ sudo du -sh /var/lib/docker/overlay2
71.8M   /var/lib/docker/overlay2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a real machine that number is whatever you had before the switch, often many gigabytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker info &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Driver}} {{json .DriverStatus}}'&lt;/span&gt;    &lt;span class="c"&gt;# io.containerd.snapshotter.v1 = containerd store&lt;/span&gt;
&lt;span class="nb"&gt;sudo ls&lt;/span&gt; /var/lib/docker/image/overlay2/layerdb/sha256 | &lt;span class="nb"&gt;wc&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;  &lt;span class="c"&gt;# layers the classic store still records&lt;/span&gt;
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sh&lt;/span&gt; /var/lib/docker/overlay2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Free it:&lt;/strong&gt; switch the daemon back to overlay2. The old images and containers reappear and can be removed normally. &lt;code&gt;docker save&lt;/code&gt; anything you want to keep first.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does &lt;em&gt;not&lt;/em&gt; hide space: containerd's compressed content
&lt;/h2&gt;

&lt;p&gt;A common suspicion is that the containerd store's second copy of each layer (compressed blobs next to unpacked snapshots) is space &lt;code&gt;df&lt;/code&gt; ignores. It is not, at least on 29.9:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker system df     → Images 296.5MB
content (compressed)     72.4 MB
snapshots (unpacked)    224.4 MB      sum 296.8 MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Images row includes both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leftover overlay2 directories
&lt;/h2&gt;

&lt;p&gt;The third classic case, &lt;a href="https://github.com/moby/moby/issues/47089" rel="noopener noreferrer"&gt;moby#47089&lt;/a&gt;: &lt;code&gt;overlay2/&lt;/code&gt; is far larger than images, containers and build cache together. The graph driver records which directory belongs to which image layer (&lt;code&gt;image/overlay2/layerdb/sha256/*/cache-id&lt;/code&gt;) and to which container (&lt;code&gt;layerdb/mounts/*/mount-id&lt;/code&gt;). A directory that appears in neither is a BuildKit snapshot or a leftover. Comparing the size of those directories with the private build cache shows whether leftovers are the likely cause. Don't delete them by hand while the daemon runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/maximilianfeix/dockdu" rel="noopener noreferrer"&gt;dockdu&lt;/a&gt;, a read-only CLI that explains Docker disk usage, does all three checks since 0.4.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;buildx builders are listed under BUILD CACHE, with the prune command for each;&lt;/li&gt;
&lt;li&gt;the hidden classic store is reported from a single directory listing;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dockdu --disk&lt;/code&gt; splits &lt;code&gt;overlay2/&lt;/code&gt; into image layers, container layers and directories with no record.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It never deletes anything, and its Docker client refuses any request other than GET/HEAD on four read endpoints. The manual commands above work without it.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>linux</category>
      <category>containers</category>
    </item>
    <item>
      <title>Why docker system df doesn't explain your full disk, and why prune frees less than it says</title>
      <dc:creator>Maxi</dc:creator>
      <pubDate>Wed, 14 Oct 2026 19:47:19 +0000</pubDate>
      <link>https://dev.to/taventix/why-docker-system-df-doesnt-explain-your-full-disk-and-why-prune-frees-less-than-it-says-27gc</link>
      <guid>https://dev.to/taventix/why-docker-system-df-doesnt-explain-your-full-disk-and-why-prune-frees-less-than-it-says-27gc</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: this article was generated by an AI agent (Claude) working on the dockdu repository, from measurements it ran on disposable Docker daemons. Every number links to its raw output and can be reproduced with the commands below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Measured on 2026-10-14 on disposable Docker Engine 28.5.2 and 29.9.0 daemons. Every number below is from &lt;a href="https://github.com/maximilianfeix/dockdu/blob/main/docs/verification/demo-2026-10-14.md" rel="noopener noreferrer"&gt;the measurement log&lt;/a&gt;; the commands work on any Linux host where you can read &lt;code&gt;/var/lib/docker&lt;/code&gt; (as root).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker system df&lt;/code&gt; is precise about what it counts. The trouble is what it leaves out, and that "reclaimable" means two different things depending on your Docker version and image store. This article goes through the four places the gap usually hides, with commands that answer each question by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Container logs are not in any &lt;code&gt;df&lt;/code&gt; row
&lt;/h2&gt;

&lt;p&gt;The default log driver, &lt;code&gt;json-file&lt;/code&gt;, has &lt;code&gt;max-size&lt;/code&gt; unlimited and &lt;code&gt;max-file&lt;/code&gt; 1 unless you configure it (&lt;a href="https://docs.docker.com/engine/logging/drivers/json-file/" rel="noopener noreferrer"&gt;docs&lt;/a&gt;). The log of a chatty container grows until the disk is full. &lt;code&gt;docker system df&lt;/code&gt; has four rows (Images, Containers, Local Volumes, Build Cache), and log files are in none of them. Containers' SIZE is the writable layer only. Adding logs to &lt;code&gt;df&lt;/code&gt; has been requested since 2017 (&lt;a href="https://github.com/moby/moby/issues/32205" rel="noopener noreferrer"&gt;moby#32205&lt;/a&gt;, &lt;a href="https://github.com/moby/moby/issues/38341" rel="noopener noreferrer"&gt;moby#38341&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Measured:&lt;/strong&gt; one container wrote 500 MiB to stdout.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          2         2         147.7MB   0B (0%)
Containers      4         4         24.58kB   0B (0%)
Local Volumes   2         1         33.55MB   0B (0%)
Build Cache     4         0         147.6MB   12.29kB

$ du -ch /var/lib/docker/containers/*/*-json.log | tail -1
535.1M  total
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The logs are larger than everything &lt;code&gt;df&lt;/code&gt; reports combined. &lt;code&gt;docker system prune&lt;/code&gt; does not touch them: in the same setup it reclaimed &lt;code&gt;0B&lt;/code&gt;, because the containers were running.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /var/lib/docker/containers/&lt;span class="k"&gt;*&lt;/span&gt;/&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="nt"&gt;-json&lt;/span&gt;.log&lt;span class="k"&gt;*&lt;/span&gt; | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; | &lt;span class="nb"&gt;tail
&lt;/span&gt;docker inspect &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s1"&gt;'{{.Name}} {{.HostConfig.LogConfig.Type}} {{.HostConfig.LogConfig.Config}}'&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;-aq&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;Set &lt;code&gt;"log-opts": {"max-size": "10m", "max-file": "3"}&lt;/code&gt; in &lt;code&gt;/etc/docker/daemon.json&lt;/code&gt;, or use the &lt;code&gt;local&lt;/code&gt; driver (20 MiB × 5 files, compressed by default).&lt;/li&gt;
&lt;li&gt;Both apply to &lt;strong&gt;new&lt;/strong&gt; containers only; existing ones must be recreated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Edge cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Rotated files&lt;/strong&gt; (&lt;code&gt;-json.log.1&lt;/code&gt;, &lt;code&gt;.2&lt;/code&gt;) count too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Truncating vs deleting:&lt;/strong&gt; deleting a log file of a running container does not free the space until the daemon closes it. A deleted file that is still open shows up in &lt;code&gt;lsof +L1&lt;/code&gt;, not in &lt;code&gt;du&lt;/code&gt;. Truncating it to zero frees it at once but discards history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other drivers:&lt;/strong&gt; with &lt;code&gt;journald&lt;/code&gt; or &lt;code&gt;syslog&lt;/code&gt; the logs live outside Docker's data root.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. Build cache "shared" with images: counted twice, freed differently
&lt;/h2&gt;

&lt;p&gt;BuildKit marks a cache record &lt;code&gt;Shared&lt;/code&gt; when its data is also part of an image. Three tools give three different answers for the same records:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Engine 28.5.2, overlay2&lt;/th&gt;
&lt;th&gt;Engine 29.9.0, containerd image store&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker system df&lt;/code&gt; Build Cache row&lt;/td&gt;
&lt;td&gt;leaves shared records out (96 B shown, 64 MiB shared)&lt;/td&gt;
&lt;td&gt;includes them (147.6 MB)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker buildx du&lt;/code&gt; Reclaimable&lt;/td&gt;
&lt;td&gt;counts shared records as reclaimable&lt;/td&gt;
&lt;td&gt;counts shared records as reclaimable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker builder prune -af&lt;/code&gt; prints&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Total: 67.11MB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Total: 147.6MB&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freed on disk (&lt;code&gt;du -sxk&lt;/code&gt; before/after)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;≈ 0&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72.7 MiB of 140.7 MiB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why the rows differ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker CLI 28 sums only records that are not shared (&lt;a href="https://github.com/docker/cli/blob/v28.5.1/cli/command/system/df.go" rel="noopener noreferrer"&gt;&lt;code&gt;cli/command/system/df.go&lt;/code&gt;&lt;/a&gt;, v28.5.1).&lt;/li&gt;
&lt;li&gt;Engine 29 (API ≥ 1.52) sends a total that includes them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why &lt;code&gt;prune&lt;/code&gt; prints a number it does not free:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On overlay2, a shared record's data belongs to the image, so pruning the record removes metadata.&lt;/li&gt;
&lt;li&gt;On the containerd image store, about half of the shared bytes were freed in two separate runs (72.7 of 140.7 MiB, 56.8 of 108.7 MiB). The build cache evidently kept its own copy of part of the data; these measurements do not show which store objects it consisted of.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this means in practice:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On overlay2,&lt;/strong&gt; expect &lt;code&gt;docker builder prune&lt;/code&gt; to free roughly the &lt;em&gt;private&lt;/em&gt; part only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On the containerd image store,&lt;/strong&gt; expect somewhere between the private part and the printed total. Measure with &lt;code&gt;du&lt;/code&gt; before and after if it matters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;When adding up &lt;code&gt;df&lt;/code&gt; rows,&lt;/strong&gt; Engine 29 counts shared bytes in both the Images and the Build Cache rows.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Check by hand:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx &lt;span class="nb"&gt;du&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'ID:|Shared:|Size:'&lt;/span&gt;      &lt;span class="c"&gt;# per record&lt;/span&gt;
docker info &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.Driver}} {{json .DriverStatus}}'&lt;/span&gt;     &lt;span class="c"&gt;# "io.containerd.snapshotter.v1" = containerd image store&lt;/span&gt;
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sxk&lt;/span&gt; /var/lib/docker      &lt;span class="c"&gt;# before and after a prune: what it really freed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fresh Engine 29.9.0 installs use the containerd image store by default (observed in &lt;code&gt;docker:29-dind&lt;/code&gt;), and so does Docker Desktop. Hosts upgraded from older versions may still be on overlay2.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Image sizes overlap
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;docker image ls&lt;/code&gt; shows each image's full size. Two images built &lt;code&gt;FROM&lt;/code&gt; the same base each include that base, so adding up image sizes, or per-project totals built from them, counts the base more than once. The Images row of &lt;code&gt;docker system df&lt;/code&gt; counts each layer once.&lt;/p&gt;

&lt;p&gt;In the demo the two images share 12.7 MiB. &lt;code&gt;docker system df -v&lt;/code&gt; shows &lt;code&gt;SHARED SIZE&lt;/code&gt; and &lt;code&gt;UNIQUE SIZE&lt;/code&gt; per image; only the unique size comes back when you remove an image that no container uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Docker Desktop: a sparse file
&lt;/h2&gt;

&lt;p&gt;On macOS, all of Docker lives in one VM disk file.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finder and &lt;code&gt;ls -l&lt;/code&gt; show its maximum size;&lt;/strong&gt; &lt;code&gt;ls -ks&lt;/code&gt; shows what is really allocated (&lt;a href="https://docs.docker.com/desktop/troubleshoot-and-support/faqs/macfaqs/" rel="noopener noreferrer"&gt;Docker Mac FAQ&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freeing space inside the VM&lt;/strong&gt; does not shrink the file until Docker Desktop compacts it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logs are invisible from the host:&lt;/strong&gt; the log files from section 1 live inside the VM, where a host-side &lt;code&gt;du&lt;/code&gt; cannot reach them. Run commands inside the VM instead, for example with &lt;code&gt;docker run --rm -v /var/lib/docker:/var/lib/docker:ro alpine du …&lt;/code&gt;.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-ks&lt;/span&gt; ~/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When none of this adds up: is it Docker's at all?
&lt;/h2&gt;

&lt;p&gt;Compare the filesystem with the data root, both in allocated blocks on the same filesystem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /var/lib/docker
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sxh&lt;/span&gt; /var/lib/docker          &lt;span class="c"&gt;# -x: do not count overlay mounts of running containers&lt;/span&gt;
&lt;span class="nb"&gt;sudo du&lt;/span&gt; &lt;span class="nt"&gt;-sxh&lt;/span&gt; /var/lib/docker/&lt;span class="k"&gt;*&lt;/span&gt;/ | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the result tells you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The data root is small:&lt;/strong&gt; the full disk is not Docker's.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;overlay2/&lt;/code&gt; or &lt;code&gt;containerd/&lt;/code&gt; is far larger than images + containers + build cache:&lt;/strong&gt; look for leftovers from crashes or interrupted pulls (&lt;a href="https://github.com/moby/moby/issues/47089" rel="noopener noreferrer"&gt;moby#47089&lt;/a&gt;, &lt;a href="https://github.com/moby/moby/issues/52539" rel="noopener noreferrer"&gt;moby#52539&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't subtract &lt;code&gt;docker system df&lt;/code&gt; numbers from &lt;code&gt;du&lt;/code&gt; numbers: one is logical sizes with shared layers counted once, the other is blocks on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-command version
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/maximilianfeix/dockdu" rel="noopener noreferrer"&gt;dockdu&lt;/a&gt;, an open-source tool by the author of this account, does the checks above in one read-only run. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;measures log files (with &lt;code&gt;lstat&lt;/code&gt; only);&lt;/li&gt;
&lt;li&gt;splits the build cache into shared and private and says what prune keeps, or that it is unknown on the containerd image store;&lt;/li&gt;
&lt;li&gt;groups everything by compose project;&lt;/li&gt;
&lt;li&gt;with &lt;code&gt;--disk&lt;/code&gt;, measures the data root next to the filesystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its Docker client refuses any request other than GET/HEAD on four read endpoints, and it never deletes anything.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;maximilianfeix/tap/dockdu      &lt;span class="c"&gt;# or the container image, see the README&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It does not attribute disk space that no API reports (overlay2 leftovers), it only counts the default BuildKit builder, and it does not run on Windows. The manual commands above work everywhere.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Reproduce every number: &lt;code&gt;scripts/demo-up.sh&lt;/code&gt; on a disposable daemon, then the commands in &lt;a href="https://github.com/maximilianfeix/dockdu/blob/main/docs/verification/demo-2026-10-14.md" rel="noopener noreferrer"&gt;the measurement log&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>linux</category>
      <category>opensource</category>
    </item>
    <item>
      <title>I checked a million free proxies. One in five of the working ones rewrote my pages.</title>
      <dc:creator>Maxi</dc:creator>
      <pubDate>Sat, 26 Sep 2026 09:47:46 +0000</pubDate>
      <link>https://dev.to/taventix/i-checked-a-million-free-proxies-one-in-five-of-the-working-ones-rewrote-my-pages-1no9</link>
      <guid>https://dev.to/taventix/i-checked-a-million-free-proxies-one-in-five-of-the-working-ones-rewrote-my-pages-1no9</guid>
      <description>&lt;p&gt;Free proxy lists look generous. Thousands of addresses, updated every few minutes. The first time I tried them for a scraping job, almost nothing connected. The second surprise came later: some of the ones that did connect were not doing what I thought.&lt;/p&gt;

&lt;p&gt;This is what I found while building &lt;a href="https://github.com/maximilianfeix/proxy-scraper" rel="noopener noreferrer"&gt;proxy-scraper&lt;/a&gt;, an open-source tool that collects public proxies and checks them properly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dead is the easy part
&lt;/h2&gt;

&lt;p&gt;Every run starts with about a million candidates from 700+ public lists. Most of them never finish a TCP handshake. That's expected and cheap to detect.&lt;/p&gt;

&lt;p&gt;The interesting failures come after that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honeypots answer the question you asked
&lt;/h2&gt;

&lt;p&gt;The usual check is: connect through the proxy, ask a "what is my IP" service, see a foreign IP, done. Some proxies answer exactly that request with a plausible "200 + IP" and fail everything else. In some runs they were five out of six "hits".&lt;/p&gt;

&lt;p&gt;The fix is a second, independent request. The proxy has to fetch a different site and show the same exit IP there too. Honeypots fall out here, and the same response tells you which headers arrive at the target, which gives you the anonymity level for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some proxies edit what you download
&lt;/h2&gt;

&lt;p&gt;This is the one that surprised me. I added a check that fetches a static HTML page through the proxy and compares its hash with the page fetched directly over verified HTTPS.&lt;/p&gt;

&lt;p&gt;Out of 270 proxies that had passed the handshake and the honeypot check, &lt;strong&gt;54 returned a modified page&lt;/strong&gt;. Almost always an injected &lt;code&gt;&amp;lt;script src="http://…"&amp;gt;&lt;/code&gt;, sometimes ads. That's 20% of the pool that looked perfectly fine by every usual measure.&lt;/p&gt;

&lt;p&gt;If you scrape through free proxies, a fifth of your responses may be rewritten on the way. If you browse through them, you're running someone else's JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the rest exits from
&lt;/h2&gt;

&lt;p&gt;With the free DB-IP ASN database the tool looks up the provider of every exit IP. In today's list, 52% of working proxies exit from datacenters, which many sites block on sight. And 28% of exit IPs are on the SpamCop blocklist, which means captchas on a lot of sites. Both can be filtered out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Learning which lists are worth it
&lt;/h2&gt;

&lt;p&gt;A proxy that worked in the last run is far more likely to work now than a random entry. So proxy-scraper checks known-good proxies first, then proxies from sources with a good hit rate, and it drops lists that went stale. Unchanged lists are skipped with ETags: a second run right after the first loads 0 MB instead of about 160 MB.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/maximilianfeix/proxy-scraper.git
proxy-scraper &lt;span class="nt"&gt;--want&lt;/span&gt; 50 &lt;span class="nt"&gt;--https-only&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--serve&lt;/code&gt; turns the result into a rotating proxy (HTTP and SOCKS5 on one port, sticky sessions, country per request through the username, optional password, refills itself in the background), there's a Python API, and &lt;code&gt;-o -&lt;/code&gt; prints the hits for pipes. If you don't want to run anything, a GitHub Action checks everything every hour and publishes the list: &lt;a href="https://maximilianfeix.github.io/proxy-scraper/" rel="noopener noreferrer"&gt;maximilianfeix.github.io/proxy-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The code is MIT licensed. If you find a check I'm missing, issues are very welcome: &lt;a href="https://github.com/maximilianfeix/proxy-scraper" rel="noopener noreferrer"&gt;github.com/maximilianfeix/proxy-scraper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Free proxies are run by strangers. Never send passwords or personal data through them.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>opensource</category>
      <category>security</category>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
