<?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: ishant singh</title>
    <description>The latest articles on DEV Community by ishant singh (@ishantsinghdev).</description>
    <link>https://dev.to/ishantsinghdev</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%2F3378161%2Fb60e831e-0656-4eb6-b2a6-0a9abfacefc0.jpg</url>
      <title>DEV Community: ishant singh</title>
      <link>https://dev.to/ishantsinghdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ishantsinghdev"/>
    <language>en</language>
    <item>
      <title>I tested my IP reputation checker on 80 addresses I knew the answer to</title>
      <dc:creator>ishant singh</dc:creator>
      <pubDate>Fri, 23 Oct 2026 10:19:37 +0000</pubDate>
      <link>https://dev.to/ishantsinghdev/i-tested-my-ip-reputation-checker-on-80-addresses-i-knew-the-answer-to-192o</link>
      <guid>https://dev.to/ishantsinghdev/i-tested-my-ip-reputation-checker-on-80-addresses-i-knew-the-answer-to-192o</guid>
      <description>&lt;p&gt;ipcomet's reputation check answers a question I get asked a lot: can I trust this address? It reports whether an IP is a Tor exit node, what kind of network owns it, and whether it appears on five public blocklists. I wrote it, which makes me the wrong person to judge it. So I found two groups of addresses where the right answer is public and ran the checker against both.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test sets
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;40 Tor exit nodes, picked at random from the Tor Project's official exit list, which had 1,226 IPv4 addresses when I downloaded it.&lt;/li&gt;
&lt;li&gt;40 addresses inside Amazon EC2, picked from the ranges AWS publishes in &lt;code&gt;ip-ranges.json&lt;/code&gt;. The file I used (created 23 October 2026) lists 1,887 EC2 prefixes. For each address I chose a random prefix, then a random host inside it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both samples used the random seed 2026. Each address got one call to &lt;code&gt;https://ipcomet.com/api/reputation&lt;/code&gt;, 1.2 seconds apart to stay under the 60 a minute limit. All 80 lookups answered.&lt;/p&gt;

&lt;p&gt;That gives me ground truth for two things. A Tor exit should be flagged as Tor and an EC2 address should not. And an EC2 address should be labelled as hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&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;Tor exits (40)&lt;/th&gt;
&lt;th&gt;EC2 addresses (40)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flagged as Tor&lt;/td&gt;
&lt;td&gt;40&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network kind "hosting"&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network kind "isp"&lt;/td&gt;
&lt;td&gt;27&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;On at least one blocklist&lt;/td&gt;
&lt;td&gt;39&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The Tor flag was exact
&lt;/h3&gt;

&lt;p&gt;All 40 exits were flagged and none of the 40 EC2 addresses were. That is the least interesting result, and I'm glad it is. The flag is a lookup in the official exit list, and I had just downloaded a fresh copy. It's also the only field in the response that doesn't rely on a guess.&lt;/p&gt;

&lt;h3&gt;
  
  
  "Hosting" caught 38 of 40 cloud addresses
&lt;/h3&gt;

&lt;p&gt;Two EC2 addresses came back as "isp". The checker returned &lt;code&gt;A100 ROW Inc&lt;/code&gt; as the owner of one and &lt;code&gt;Langfang university Langfang Development Area&lt;/code&gt; for the other, and neither name looks like a hosting company. The network kind comes from who owns the address (plus hints in its reverse DNS name), so when the registered owner doesn't look like a hosting provider, the label slips. I didn't dig into why AWS-published prefixes are registered to those names.&lt;/p&gt;

&lt;h3&gt;
  
  
  Most Tor exits were labelled as ordinary ISPs
&lt;/h3&gt;

&lt;p&gt;This is the result I didn't expect. 27 of the 40 Tor exits came back with the kind "isp", and the label text for that kind reads "Likely a residential or mobile ISP". The 27 spread across 15 different organisations, so one operator isn't skewing it. The &lt;code&gt;tor&lt;/code&gt; field was right every time. The sentence next to it was misleading for two out of three of these addresses.&lt;/p&gt;

&lt;p&gt;If you read only the network kind, you would have waved most of the exits through.&lt;/p&gt;

&lt;h3&gt;
  
  
  The blocklists agreed less than they look
&lt;/h3&gt;

&lt;p&gt;39 of the 40 Tor exits appeared on at least one of the five blocklists, and none of the EC2 addresses did. But one list, s5h.net, accounts for 39 of those hits by itself. DroneBL listed 15 of the exits, SpamCop 9 and UCEPROTECT Level 1 only 2. So "listed on 3 of 5" can mean one list that tracks Tor exits plus two more, and in this sample s5h.net behaved like a Tor list.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I'd use the result
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Trust the Tor flag.&lt;/li&gt;
&lt;li&gt;Treat "hosting" as a strong hint that can miss. It caught 38 of 40 here.&lt;/li&gt;
&lt;li&gt;Read "isp" as "no hosting signs found", and nothing more about whoever is using the address.&lt;/li&gt;
&lt;li&gt;Count blocklist hits per list, not in total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As the maintainer, the part I take from this is the wording of the "isp" label, which promises more than the data supports.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this test can't show
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;It has no home connections in it. There's no public list of residential addresses, so I can't measure how often a real home gets labelled as hosting, or how many VPNs and proxies get through. The about page already says residential proxies can't be detected from public data.&lt;/li&gt;
&lt;li&gt;It's two samples of 40 from one moment, and only one cloud provider.&lt;/li&gt;
&lt;li&gt;The Tor list changes through the day, so the same seed can pick different exits tomorrow.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Run it yourself
&lt;/h2&gt;

&lt;p&gt;The script I used, tidied up. It needs only the standard library. Set your own contact in the &lt;code&gt;User-Agent&lt;/code&gt;, as the &lt;a href="https://dev.to/ishantsinghdev/python-got-a-403-and-curl-didnt-it-came-down-to-one-case-sensitive-string-4fjp"&gt;last post&lt;/a&gt; explains.&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;collections&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ipaddress&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="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="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="n"&gt;UA&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;User-Agent&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;reputation-check/1.0 (+you@example.com)&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="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
&lt;span class="n"&gt;rng&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="nc"&gt;Random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# same seed, same sample, if the lists haven't changed
&lt;/span&gt;
&lt;span class="k"&gt;def&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="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;3&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;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;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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;UA&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="nf"&gt;read&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;OSError&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="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;giving up on &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&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;tor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;l&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="ow"&gt;in&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://check.torproject.org/torbulkexitlist&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;splitlines&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;l&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="ow"&gt;and&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="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;ranges&lt;/span&gt; &lt;span class="o"&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;loads&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://ip-ranges.amazonaws.com/ip-ranges.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;ec2&lt;/span&gt; &lt;span class="o"&gt;=&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ip_prefix&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;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prefixes&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;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;service&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;EC2&lt;/span&gt;&lt;span class="sh"&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;random_host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;net&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ipaddress&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip_network&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&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;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;network_address&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;randrange&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="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_addresses&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="n"&gt;groups&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;tor exit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tor&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;aws ec2&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="nf"&gt;random_host&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;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ec2&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ips&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;groups&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="o"&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;ip&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;seen&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;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&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://ipcomet.com/api/reputation?q=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;ip&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="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# the endpoint allows 60 a minute
&lt;/span&gt;    &lt;span class="n"&gt;kinds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;collections&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="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;network&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;kind&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&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;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: tor flag &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&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;tor&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&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;N&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="s"&gt;on a blocklist &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;sum&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;flagged&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&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;N&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, network kinds &lt;/span&gt;&lt;span class="si"&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;kinds&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It takes about two minutes. The full response for each address is documented in the &lt;a href="https://ipcomet.com/developers" rel="noopener noreferrer"&gt;API reference&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I maintain &lt;a href="https://ipcomet.com" rel="noopener noreferrer"&gt;ipcomet&lt;/a&gt;, a free IP checker. AI-assisted: I used Claude to help draft this post and to run the test. The lookups were made from my own machine, and every request to the checker went to my own API.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>security</category>
      <category>python</category>
      <category>testing</category>
    </item>
    <item>
      <title>Python got a 403 and curl didn't. It came down to one case-sensitive string</title>
      <dc:creator>ishant singh</dc:creator>
      <pubDate>Fri, 23 Oct 2026 08:22:35 +0000</pubDate>
      <link>https://dev.to/ishantsinghdev/python-got-a-403-and-curl-didnt-it-came-down-to-one-case-sensitive-string-4fjp</link>
      <guid>https://dev.to/ishantsinghdev/python-got-a-403-and-curl-didnt-it-came-down-to-one-case-sensitive-string-4fjp</guid>
      <description>&lt;p&gt;A few days ago I wrote about &lt;a href="https://dev.to/ishantsinghdev/how-to-get-your-public-ip-address-from-the-command-line-kd8"&gt;getting your public IP from the command line&lt;/a&gt;. In the Python section I mentioned, almost in passing, that the default &lt;code&gt;urllib&lt;/code&gt; gets a 403 from ipcomet while curl doesn't. I wrote that line from memory. It kept bothering me, because "bot protection rejects it" isn't an explanation. So I tested it properly.&lt;/p&gt;

&lt;p&gt;Same laptop, same connection, same IP, same endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl https://ipcomet.com/api/ip              -&amp;gt; 200
Python urllib, default settings              -&amp;gt; 403
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Something differs between those two requests. Finding out exactly what took less time than I expected, and the answer is narrower than "Python gets blocked".&lt;/p&gt;

&lt;h2&gt;
  
  
  First suspect: the headers
&lt;/h2&gt;

&lt;p&gt;If a server tells two clients apart, it's looking at what they send. So I wrote a tiny server that echoes back the request headers it receives, ran it on localhost (no third party involved), and hit it with four clients.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# echo_headers.py: returns the headers each client sends, in order
&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;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HTTPServer&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Echo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&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;do_GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&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="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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="nf"&gt;items&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&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;application/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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Length&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&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;body&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;log_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&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="k"&gt;pass&lt;/span&gt;

&lt;span class="nc"&gt;HTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8777&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Echo&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What came back, in the order the headers arrived:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Client&lt;/th&gt;
&lt;th&gt;Headers sent&lt;/th&gt;
&lt;th&gt;&lt;code&gt;User-Agent&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Accept-Encoding&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;curl 8.7.1&lt;/td&gt;
&lt;td&gt;host, user-agent, accept (3)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl/8.7.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;not sent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;curl --compressed&lt;/td&gt;
&lt;td&gt;host, user-agent, accept, accept-encoding (4)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;curl/8.7.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;deflate, gzip&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 3.9 urllib&lt;/td&gt;
&lt;td&gt;accept-encoding, host, user-agent, connection (4)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Python-urllib/3.9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;identity&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node 24 fetch&lt;/td&gt;
&lt;td&gt;host, connection, accept, accept-language, sec-fetch-mode, user-agent, accept-encoding (7)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;node&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gzip, deflate&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Nothing here looks more "bot-like" for urllib than for curl. curl sends the fewest headers of all and sails through, so the header count isn't it. urllib does send two things curl doesn't by default: &lt;code&gt;Accept-Encoding: identity&lt;/code&gt;, which tells the server not to compress anything, and &lt;code&gt;Connection: close&lt;/code&gt;. Neither looks unusual enough to justify a block.&lt;/p&gt;

&lt;p&gt;That left the &lt;code&gt;User-Agent&lt;/code&gt;, the one header whose value differs in an obvious way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Same client, different User-Agent
&lt;/h2&gt;

&lt;p&gt;To separate the header from everything else, I stopped changing the client and changed only the string. Same curl, same machine, one request per &lt;code&gt;User-Agent&lt;/code&gt;, 1.5 seconds apart, against &lt;code&gt;https://ipcomet.com/api/ip&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;User-Agent&lt;/code&gt; sent&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;curl/8.7.1&lt;/code&gt; (default)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Python-urllib/3.9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Python-urllib/3.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Python-urllib&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;403&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;python-urllib/3.9&lt;/code&gt; (lowercase)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;urllib&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;my-urllib-script/1.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python-requests/2.31.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python-httpx/0.27.0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Python/3.9 aiohttp/3.9.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Go-http-client/1.1&lt;/code&gt;, &lt;code&gt;Wget/1.21.4&lt;/code&gt;, &lt;code&gt;node&lt;/code&gt;, &lt;code&gt;axios/1.6.0&lt;/code&gt;, &lt;code&gt;okhttp/4.12.0&lt;/code&gt;, &lt;code&gt;Java/17.0.2&lt;/code&gt;, &lt;code&gt;PostmanRuntime/7.36.0&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;200 (all)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty (no &lt;code&gt;User-Agent&lt;/code&gt; header)&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;my-script/1.0&lt;/code&gt;, with or without contact details appended&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Twenty strings, 22 requests. Exactly one family was blocked: anything containing &lt;code&gt;Python-urllib&lt;/code&gt;, with that capitalisation. The lowercase version passed. So did &lt;code&gt;urllib&lt;/code&gt; alone and &lt;code&gt;my-urllib-script/1.0&lt;/code&gt;, and so did every other non-browser client I tried, including an empty &lt;code&gt;User-Agent&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two reverse checks agree. curl sending &lt;code&gt;Python-urllib/3.9&lt;/code&gt; got the 403 all three times I tried it, and Python's urllib sending &lt;code&gt;my-script/1.0&lt;/code&gt; got a 200.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the 403 looks like
&lt;/h2&gt;

&lt;p&gt;The response is a 403 with a body of 17 bytes that contains the code &lt;code&gt;1010&lt;/code&gt;. That is Cloudflare's error for access denied based on the browser's signature, and ipcomet is served through Cloudflare. I didn't trace which rule produced it. The matrix already tells a client everything it needs to know about what the rule matches. It's a plain string match on a known default, which is why it's so easy to hit by accident and so confusing to debug. Nothing about the response says "change your User-Agent".&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do about it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Set a descriptive &lt;code&gt;User-Agent&lt;/code&gt; on every script, something like &lt;code&gt;my-tool/1.0 (+you@example.com)&lt;/code&gt;. It costs nothing, and it gives a site owner someone to contact instead of a reason to block you.&lt;/li&gt;
&lt;li&gt;Don't copy a browser's string to get around a block. A blocked default is a hint to identify yourself properly, and pretending to be Chrome sends the wrong signal.&lt;/li&gt;
&lt;li&gt;When a script gets a 403 with a tiny body, check the &lt;code&gt;User-Agent&lt;/code&gt; first. It takes ten seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In urllib the fix is one argument:&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;urllib.request&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://ipcomet.com/api/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;headers&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;User-Agent&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;my-tool/1.0 (+you@example.com)&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="nf"&gt;print&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="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;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Run the same test on your own site
&lt;/h2&gt;

&lt;p&gt;This is the script I used, rewritten to use only the standard library. &lt;strong&gt;Only point it at a site you own or have permission to test.&lt;/strong&gt; It sends one request per string with a pause between them.&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;sys&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="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&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="n"&gt;URL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="n"&gt;AGENTS&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;curl/8.7.1&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;Python-urllib/3.9&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;Python-urllib/3.12&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;Python-urllib&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;python-urllib/3.9&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;urllib&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;my-urllib-script/1.0&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;python-requests/2.31.0&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;python-httpx/0.27.0&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;Go-http-client/1.1&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;Wget/1.21.4&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;node&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;axios/1.6.0&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;okhttp/4.12.0&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;Java/17.0.2&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;PostmanRuntime/7.36.0&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;my-script/1.0&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&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="n"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;agent&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;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;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;
    &lt;span class="k"&gt;except&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;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HTTPError&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="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&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="c1"&gt;# timeouts and resets are results too
&lt;/span&gt;        &lt;span class="k"&gt;return&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AGENTS&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;agent&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;26&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;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;agent&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="mf"&gt;1.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with &lt;code&gt;python3 ua_matrix.py https://your-site.example/&lt;/code&gt;. I ran exactly this version against ipcomet as well, with urllib doing the asking instead of curl, and every status matched the table above.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;It's one site's configuration. Other sites, including most of the ones you scrape, will behave differently. This maps one rule.&lt;/li&gt;
&lt;li&gt;I tested the libraries by their strings. &lt;code&gt;requests&lt;/code&gt; and &lt;code&gt;httpx&lt;/code&gt; weren't installed on this machine, so those rows come from curl sending their &lt;code&gt;User-Agent&lt;/code&gt;. The real libraries also send other headers of their own, which could matter on a stricter site.&lt;/li&gt;
&lt;li&gt;Rules change. This is what one site did when I tested it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your lookups to ipcomet fail with a 403, the &lt;a href="https://ipcomet.com/developers" rel="noopener noreferrer"&gt;API reference&lt;/a&gt; already lists this under client notes.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I maintain &lt;a href="https://ipcomet.com" rel="noopener noreferrer"&gt;ipcomet&lt;/a&gt;, a free IP checker. AI-assisted: I used Claude to help draft this post and to run the tests. The results come from my own machine, and every request in the matrix went to my own site.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>http</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Where is 8.8.8.8? I checked three IP databases against the speed of light</title>
      <dc:creator>ishant singh</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:27:52 +0000</pubDate>
      <link>https://dev.to/ishantsinghdev/where-is-8888-i-checked-three-ip-databases-against-the-speed-of-light-50d7</link>
      <guid>https://dev.to/ishantsinghdev/where-is-8888-i-checked-three-ip-databases-against-the-speed-of-light-50d7</guid>
      <description>&lt;p&gt;I maintain ipcomet, a free IP lookup site, and one question reaches me more than any other: "why does it say I'm in the wrong city?" Normally I explain that geolocation is an estimate and leave it there. This time I wanted numbers.&lt;/p&gt;

&lt;p&gt;So I picked eight well-known IPs (four public DNS resolvers, a CDN edge, github.com, GitHub Pages and apple.com), asked several geolocation sources where each one lives, and then compared their answers with the one thing a database can't fake: how long a packet takes to get there and back.&lt;/p&gt;

&lt;p&gt;The short version. The sources agreed on the country every time they gave one. They disagreed on the city for half of the IPs. And for seven of the eight, the city they named is farther from my connection than my ping time allows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;p&gt;Light in optical fibre travels about 200 km per millisecond. That gives a hard ceiling. If a round trip takes 12 ms, the packet had 6 ms each way, so whatever answered me is at most 1,200 km from where I sit. Real cables bend and real routers queue, so the true distance is always smaller. The ceiling is generous, which means that when a claimed city is beyond it, the claim can't be right for the machine that replied.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;physical limit (km) = best round-trip time (ms) / 2 * 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter more than they look:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use the best ping, not the average.&lt;/strong&gt; Queuing only ever adds delay, so the minimum over many pings is the closest you'll get to the real distance. My first run caught 8.8.8.8 at 12.7 ms. A later run, on a congested minute, measured 133 ms for the same IP from the same laptop, and the "impossible" verdict would have vanished. A third run said 15.2 ms. I ran the pings three times and kept the best value for each IP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the same coordinates every source returns.&lt;/strong&gt; Each source gives a latitude and longitude. I measured the distance from my own connection's position (taken from ipcomet's &lt;code&gt;/api/me&lt;/code&gt;) to each claimed point. Every claimed city was more than 10,000 km away from me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sources were ipwho.is, ipapi.is and geojs.io (all free, no key) plus ipcomet's own &lt;code&gt;/api/lookup&lt;/code&gt;. More on that last one at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What came back
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;IP&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Best ping&lt;/th&gt;
&lt;th&gt;Light limit&lt;/th&gt;
&lt;th&gt;ipwho.is says&lt;/th&gt;
&lt;th&gt;ipapi.is says&lt;/th&gt;
&lt;th&gt;Claim fits?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;8.8.8.8&lt;/td&gt;
&lt;td&gt;Google DNS&lt;/td&gt;
&lt;td&gt;12.7 ms&lt;/td&gt;
&lt;td&gt;1,268 km&lt;/td&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;Mountain View&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.1.1.1&lt;/td&gt;
&lt;td&gt;Cloudflare DNS&lt;/td&gt;
&lt;td&gt;12.8 ms&lt;/td&gt;
&lt;td&gt;1,283 km&lt;/td&gt;
&lt;td&gt;Brisbane&lt;/td&gt;
&lt;td&gt;Brisbane&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9.9.9.9&lt;/td&gt;
&lt;td&gt;Quad9 DNS&lt;/td&gt;
&lt;td&gt;9.8 ms&lt;/td&gt;
&lt;td&gt;981 km&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;Berkeley&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;208.67.222.222&lt;/td&gt;
&lt;td&gt;OpenDNS&lt;/td&gt;
&lt;td&gt;46.2 ms&lt;/td&gt;
&lt;td&gt;4,624 km&lt;/td&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;151.101.1.69&lt;/td&gt;
&lt;td&gt;Fastly edge&lt;/td&gt;
&lt;td&gt;11.6 ms&lt;/td&gt;
&lt;td&gt;1,158 km&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;17.253.144.10&lt;/td&gt;
&lt;td&gt;apple.com&lt;/td&gt;
&lt;td&gt;11.4 ms&lt;/td&gt;
&lt;td&gt;1,140 km&lt;/td&gt;
&lt;td&gt;San Jose&lt;/td&gt;
&lt;td&gt;Cupertino&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;185.199.108.153&lt;/td&gt;
&lt;td&gt;GitHub Pages&lt;/td&gt;
&lt;td&gt;51.1 ms&lt;/td&gt;
&lt;td&gt;5,109 km&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;Oakland City&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;140.82.112.3&lt;/td&gt;
&lt;td&gt;github.com&lt;/td&gt;
&lt;td&gt;227.5 ms&lt;/td&gt;
&lt;td&gt;22,749 km&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;San Francisco&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few things jump out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Country was never the problem.&lt;/strong&gt; Wherever a source returned a country, it matched the others. geojs.io only ever returned a country for these IPs (no city at all, and for 1.1.1.1 it had no coordinates), so I left it out of the table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;City agreement was a coin flip.&lt;/strong&gt; ipwho.is and ipapi.is gave the same city for four of the eight IPs. For 8.8.8.8 one said San Jose and the other Mountain View. For 9.9.9.9 it was San Francisco against Berkeley. Those are neighbours, but a lookup that says "San Jose" and another that says "Berkeley" for the same address can't both be telling you where a server physically sits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;github.com was the only claim that survived.&lt;/strong&gt; At 227 ms the physical limit stretches around the planet, so the check can't rule anything out. Every other row failed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the answers are "wrong" (and why that's not quite the right word)
&lt;/h2&gt;

&lt;p&gt;Geolocation databases are mostly answering a different question from the one we ask. They map an IP block to a place, usually where the block's owner is registered or based. They are not tracking where the machine answering you is plugged in.&lt;/p&gt;

&lt;p&gt;1.1.1.1 is the cleanest example. All the sources put it in Brisbane. I asked ipcomet's WHOIS endpoint what the registry says:&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="nv"&gt;$ &lt;/span&gt;curl &lt;span class="s2"&gt;"https://ipcomet.com/api/whois?q=1.1.1.1"&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"kind"&lt;/span&gt;:&lt;span class="s2"&gt;"ip"&lt;/span&gt;,&lt;span class="s2"&gt;"query"&lt;/span&gt;:&lt;span class="s2"&gt;"1.1.1.1"&lt;/span&gt;,&lt;span class="s2"&gt;"name"&lt;/span&gt;:&lt;span class="s2"&gt;"APNIC-LABS"&lt;/span&gt;,&lt;span class="s2"&gt;"cidr"&lt;/span&gt;:&lt;span class="s2"&gt;"1.1.1.0/24"&lt;/span&gt;,
 &lt;span class="s2"&gt;"type"&lt;/span&gt;:&lt;span class="s2"&gt;"ASSIGNED PORTABLE"&lt;/span&gt;,&lt;span class="s2"&gt;"country"&lt;/span&gt;:&lt;span class="s2"&gt;"AU"&lt;/span&gt;,&lt;span class="s2"&gt;"organization"&lt;/span&gt;:&lt;span class="s2"&gt;"APNICRANDNET Infrastructure Contact"&lt;/span&gt;, ...&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The block is registered to APNIC Labs, country AU, and APNIC is based in Brisbane. Meanwhile Cloudflare's resolver answered me in under 13 ms, so a server very near my connection replied. Both facts are true. The database describes the registration, the ping describes the server.&lt;/p&gt;

&lt;p&gt;The same logic explains the other DNS resolvers and the CDN edge. Those addresses are announced from many places at once (anycast), and whichever node is closest to you answers. A database has to pick one point to show, and it picks the headquarters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take from this
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Country-level answers are fine for most things.&lt;/strong&gt; Everything agreed on it. It still isn't a legal boundary, so don't build compliance on a free database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the city as a hint.&lt;/strong&gt; Two decent sources disagreed on half of a small, famous sample. Don't show a user "Berkeley" as a fact.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;For anycast, CDN and DNS addresses, ignore the location.&lt;/strong&gt; It tells you who owns the block and nothing about where the server is.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A ping is a cheap lie detector.&lt;/strong&gt; If you ever need to sanity-check a location claim (a proxy's advertised country, say), the check below takes a few lines.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This is a trimmed version of what I ran: one IP in, a verdict out. It uses only the standard library plus &lt;code&gt;ping&lt;/code&gt;, so it works on macOS and Linux.&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;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;,&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;subprocess&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&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;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;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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;User-Agent&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;geo-check/1.0&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="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;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;15&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;km&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# great-circle distance (haversine)
&lt;/span&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;la1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;la2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo2&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="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&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;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;la2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;la1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;la1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;la2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;lo2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;lo1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;6371&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&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;best_rtt_ms&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="n"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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;subprocess&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping&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;-c&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;count&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-q&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="n"&gt;capture_output&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;text&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;stdout&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&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;search&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;= ([\d.]+)/&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;group&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="c1"&gt;# first number is the minimum
&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;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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="n"&gt;me&lt;/span&gt; &lt;span class="o"&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://ipcomet.com/api/me&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&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;https://ipcomet.com/api/lookup?q=&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="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;latitude&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;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# ipcomet omits fields it can't determine
&lt;/span&gt;    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no coordinates for this 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;claimed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;km&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;me&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&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;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;latitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;longitude&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;span class="n"&gt;rtt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;best_rtt_ms&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="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rtt&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;  &lt;span class="c1"&gt;# light in fibre: ~200 km per ms, one way
&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;ip&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: database says &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;target&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="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;target&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="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="s"&gt;claimed distance : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;claimed&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&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; km&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="s"&gt;best round trip  : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rtt&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; ms  -&amp;gt;  physical limit &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;,.&lt;/span&gt;&lt;span class="mi"&gt;0&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; km&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;verdict          :&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;IMPOSSIBLE&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;claimed&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;plausible&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;Run it with &lt;code&gt;python3 geo_check.py 8.8.8.8&lt;/code&gt;. This is what it printed for me, on a bad minute for my connection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8.8.8.8: database says Mountain View, United States
claimed distance : 12,195 km
best round trip  : 55.8 ms  -&amp;gt;  physical limit 5,582 km
verdict          : IMPOSSIBLE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The best ping here is far worse than the 12.7 ms in the table, which is exactly the noise problem from earlier. The verdict still holds, because even a sluggish link can't turn 12,195 km into 5,582 km. A bad ping can hide an impossible claim. It can't invent one.&lt;/p&gt;

&lt;p&gt;Two bugs bit me while I built the full version, and they're worth knowing about. geojs.io returns the &lt;em&gt;string&lt;/em&gt; &lt;code&gt;"nil"&lt;/code&gt; for coordinates it doesn't have instead of leaving the field out, so a plain &lt;code&gt;float()&lt;/code&gt; on it crashes. And on the first full run, a single TLS handshake timeout killed the whole script, because free APIs do flake. Wrap each lookup in a retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  About ipcomet's numbers
&lt;/h2&gt;

&lt;p&gt;I should be upfront about the fourth source. ipcomet's lookup is built on upstream providers (the about page lists them), not on a database of its own. In my runs its city matched ipapi.is for all eight IPs, so counting it as an independent fourth opinion would be double-counting. What it gives you is one key-free endpoint for lookup, WHOIS, DNS and reputation. That WHOIS call is what settled the 1.1.1.1 question above.&lt;/p&gt;

&lt;p&gt;The full endpoint list, with rate limits, is at &lt;a href="https://ipcomet.com/developers" rel="noopener noreferrer"&gt;ipcomet.com/developers&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I maintain &lt;a href="https://ipcomet.com" rel="noopener noreferrer"&gt;ipcomet&lt;/a&gt;, a free IP checker. AI-assisted: I used Claude to help draft this post and to run the comparison. The measurements come from my own connection and the script above is a trimmed copy of the one I ran.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>networking</category>
      <category>python</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Get Your Public IP Address From the Command Line</title>
      <dc:creator>ishant singh</dc:creator>
      <pubDate>Sat, 17 Oct 2026 03:46:08 +0000</pubDate>
      <link>https://dev.to/ishantsinghdev/how-to-get-your-public-ip-address-from-the-command-line-kd8</link>
      <guid>https://dev.to/ishantsinghdev/how-to-get-your-public-ip-address-from-the-command-line-kd8</guid>
      <description>&lt;p&gt;The quickest way to get your public IP address from a terminal is &lt;code&gt;curl https://ipcomet.com&lt;/code&gt;. It prints your address and nothing else, so the output can go straight into a script. This guide shows the same one-liner on macOS, Linux and Windows, how to do it from Python and Node.js, how to get your location and ISP as JSON, and how to choose between IPv4 and IPv6.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line answer
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://ipcomet.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The output is a single line such as &lt;code&gt;203.0.113.7&lt;/code&gt;. The home page recognises command-line clients (curl, wget, PowerShell and similar) by their User-Agent and returns just the address instead of the web page. In scripts, use the explicit endpoint &lt;code&gt;https://ipcomet.com/api/ip&lt;/code&gt;, which always returns plain text.&lt;/p&gt;

&lt;h2&gt;
  
  
  macOS and Linux
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://ipcomet.com/api/ip
wget &lt;span class="nt"&gt;-qO-&lt;/span&gt; https://ipcomet.com/api/ip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;macOS includes curl but not wget. To keep the result in a shell variable, use &lt;code&gt;MYIP=$(curl -s https://ipcomet.com/api/ip)&lt;/code&gt;. The &lt;code&gt;-s&lt;/code&gt; flag hides curl's progress meter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Windows
&lt;/h2&gt;

&lt;p&gt;Windows 10 and 11 ship with curl, so &lt;code&gt;curl.exe https://ipcomet.com/api/ip&lt;/code&gt; works in both Command Prompt and PowerShell. Type &lt;code&gt;curl.exe&lt;/code&gt; rather than &lt;code&gt;curl&lt;/code&gt; in PowerShell, where &lt;code&gt;curl&lt;/code&gt; can be an alias for &lt;code&gt;Invoke-WebRequest&lt;/code&gt;. The native PowerShell way is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Invoke-RestMethod&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://ipcomet.com/api/ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Trim&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;h2&gt;
  
  
  Python
&lt;/h2&gt;

&lt;p&gt;With the &lt;code&gt;requests&lt;/code&gt; library:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&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://ipcomet.com/api/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;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With only the standard library, set a User-Agent. Python's default one (&lt;code&gt;Python-urllib&lt;/code&gt;) is rejected by the bot protection in front of many sites, ipcomet included, which answer with a 403 error (Cloudflare error 1010):&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;urllib.request&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://ipcomet.com/api/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;headers&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;User-Agent&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;my-script/1.0&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="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;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  JavaScript and Node.js
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://ipcomet.com/api/ip&lt;/span&gt;&lt;span class="dl"&gt;"&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="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works in Node.js 18 and later, which has &lt;code&gt;fetch&lt;/code&gt; built in, and in a browser page, because the endpoint allows cross-origin requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get your location and ISP as JSON
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl https://ipcomet.com/api/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response describes the connection that made the request. An example, with made-up values:&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;"ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"203.0.113.7"&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="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"city"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mumbai"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"region"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Maharashtra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"country"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"India"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"countryCode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timezone"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Asia/Kolkata"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;64500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Broadband Ltd"&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;The full response also has the postal code, latitude and longitude, and a few details about the connection (the edge location, the HTTP and TLS versions and the round-trip time). To pick out one field, pipe it through &lt;code&gt;jq&lt;/code&gt;: &lt;code&gt;curl -s https://ipcomet.com/api/me | jq -r .country&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To look up any other address, use &lt;code&gt;curl "https://ipcomet.com/api/lookup?q=8.8.8.8"&lt;/code&gt;. It returns the IP version, hostname, city, region, country, coordinates, timezone, ASN and network owner. Lookups are limited to 60 a minute per caller, and location data is approximate because it comes from network records, not GPS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Force IPv4 or IPv6
&lt;/h2&gt;

&lt;p&gt;If your connection has both, add &lt;code&gt;-4&lt;/code&gt; or &lt;code&gt;-6&lt;/code&gt; to curl: &lt;code&gt;curl -4 https://ipcomet.com&lt;/code&gt; forces IPv4, and &lt;code&gt;curl -6 https://ipcomet.com&lt;/code&gt; forces IPv6, which only works if your connection has IPv6.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other services you will see recommended
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;curl ifconfig.me&lt;/code&gt;, &lt;code&gt;curl icanhazip.com&lt;/code&gt;, &lt;code&gt;curl api.ipify.org&lt;/code&gt; and &lt;code&gt;curl ipinfo.io/ip&lt;/code&gt; are other free services that return a bare address in the same way.&lt;/li&gt;
&lt;li&gt;Without HTTP, you can ask a DNS resolver instead: &lt;code&gt;dig +short myip.opendns.com @resolver1.opendns.com&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They all report the address the internet sees, so they should agree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why do two services show different IP addresses?
&lt;/h2&gt;

&lt;p&gt;If they differ, your router may be sharing traffic across two internet connections, or a VPN may be carrying only some of your traffic.&lt;/p&gt;

&lt;p&gt;These commands return your &lt;em&gt;public&lt;/em&gt; address, the one websites see. For the private address on your own network, such as 192.168.1.20, check your network settings or run &lt;code&gt;ipconfig&lt;/code&gt; on Windows and &lt;code&gt;ip addr&lt;/code&gt; on Linux.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits and privacy
&lt;/h2&gt;

&lt;p&gt;No key or sign-up is needed. Please keep requests reasonable: checking every few minutes is fine, but calling it in a tight loop may be rate limited. ipcomet does not store or log the addresses it sees. The &lt;a href="https://ipcomet.com/developers" rel="noopener noreferrer"&gt;free IP API page&lt;/a&gt; lists every endpoint with its exact rate limit.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This guide first appeared on &lt;a href="https://ipcomet.com/learn/get-public-ip-address-command-line" rel="noopener noreferrer"&gt;ipcomet&lt;/a&gt;, a free IP checker I maintain.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>networking</category>
      <category>devops</category>
    </item>
    <item>
      <title>How We Built FreeCustom.Email — A Bold Step Toward Email Freedom.</title>
      <dc:creator>ishant singh</dc:creator>
      <pubDate>Tue, 22 Jul 2025 09:40:31 +0000</pubDate>
      <link>https://dev.to/ishantsinghdev/how-we-built-freecustomemail-a-bold-step-toward-email-freedom-33oe</link>
      <guid>https://dev.to/ishantsinghdev/how-we-built-freecustomemail-a-bold-step-toward-email-freedom-33oe</guid>
      <description>&lt;p&gt;With many products built on the same template, and extremely powerful forces in tech, a question crept into my mind: why can email not be as personal as the names we use on the platform?&lt;/p&gt;

&lt;p&gt;This query is what gave birth to what is now FreeCustom.Email a place to help email feel as though it were yours once again.&lt;/p&gt;

&lt;p&gt;*&lt;em&gt;🌱 The Frustration That Sparked an Idea&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
It began as what most stories about startups do; Frustration.&lt;/p&gt;

&lt;p&gt;Being a freelancer and a builder in the tech world, having an unpleasant email address has been big a problem: usually long, difficult to memorize where the email comes through free companies or, on custom domains through more advanced providers, costly to register (but need to configure). I desired an email personality that was easy, business like and mine — without the baggage.&lt;/p&gt;

&lt;p&gt;But when I looked around, everything was either:&lt;/p&gt;

&lt;p&gt;Too complicated to set up,&lt;br&gt;
Too expensive to maintain,&lt;br&gt;
Or just not flexible enough for real people.&lt;br&gt;
So I decided to change that.&lt;/p&gt;

&lt;p&gt;💡 &lt;a href="https://www.freecustom.email/" rel="noopener noreferrer"&gt;FreeCustom.Email&lt;/a&gt;: A Vision of Simplicity and Control&lt;br&gt;
&lt;a href="https://www.freecustom.email/&lt;br&gt;%0A![%20](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vbx9ec0eqzgtx2orhpzj.png)" rel="noopener noreferrer"&gt;FreeCustom.Email&lt;/a&gt; was born with a core mission:- “To give everyone the power to create their own professional email identity — instantly, affordably, and without technical headaches”.&lt;/p&gt;

&lt;p&gt;You can choose your own subdomain like &lt;a href="mailto:yourname@startuphero.email"&gt;yourname@startuphero.email&lt;/a&gt; or &lt;a href="mailto:contact@creativesoul.email"&gt;contact@creativesoul.email&lt;/a&gt;, without needing to buy or manage a full domain.&lt;/p&gt;

&lt;p&gt;No hidden fees. No shady data collection. No lock-in.&lt;/p&gt;

&lt;p&gt;Just email — your way.&lt;/p&gt;

&lt;p&gt;🌍 A Tool for Freelancers, Creators, Founders — and Everyone in Between&lt;br&gt;
What surprised us most? The diversity of our users.&lt;/p&gt;

&lt;p&gt;A solo founder in Germany using &lt;a href="mailto:founder@launchlabs.email"&gt;founder@launchlabs.email&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;An indie artist in India sending newsletters from &lt;a href="mailto:hello@canvasdreams.email"&gt;hello@canvasdreams.email&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A crypto dev who wanted privacy but professionalism.&lt;/p&gt;

&lt;p&gt;Our inboxes filled with support messages, but also with gratitude. People felt seen. They felt like their online identity finally had a home.&lt;/p&gt;

&lt;p&gt;🔮 What’s Next for FreeCustom.Email?&lt;br&gt;
We’re just getting started.&lt;/p&gt;

&lt;p&gt;More TLDs. More features. Closer integrations with productivity tools and calendar. Mobile first interface. And a duty to make sure that every time we build, we keep the center of it privacy and freedom. Non-alienating enough email identities to exhaust you? Or you simply desire a service that does not waste your time, as well as your information? Give us a look.&lt;/p&gt;

&lt;p&gt;It’s more than just an email address.&lt;/p&gt;

&lt;p&gt;It’s your identity, on your terms.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://www.freecustom.email/" rel="noopener noreferrer"&gt;FreeCustom.Email&lt;/a&gt; — Claim yours today.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>java</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
