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.
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.
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.
The method
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.
physical limit (km) = best round-trip time (ms) / 2 * 200
Two details that matter more than they look:
- Use the best ping, not the average. 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.
-
Compare the same coordinates every source returns. Each source gives a latitude and longitude. I measured the distance from my own connection's position (taken from ipcomet's
/api/me) to each claimed point. Every claimed city was more than 10,000 km away from me.
The sources were ipwho.is, ipapi.is and geojs.io (all free, no key) plus ipcomet's own /api/lookup. More on that last one at the end.
What came back
| IP | What it is | Best ping | Light limit | ipwho.is says | ipapi.is says | Claim fits? |
|---|---|---|---|---|---|---|
| 8.8.8.8 | Google DNS | 12.7 ms | 1,268 km | San Jose | Mountain View | No |
| 1.1.1.1 | Cloudflare DNS | 12.8 ms | 1,283 km | Brisbane | Brisbane | No |
| 9.9.9.9 | Quad9 DNS | 9.8 ms | 981 km | San Francisco | Berkeley | No |
| 208.67.222.222 | OpenDNS | 46.2 ms | 4,624 km | San Jose | San Jose | No |
| 151.101.1.69 | Fastly edge | 11.6 ms | 1,158 km | San Francisco | San Francisco | No |
| 17.253.144.10 | apple.com | 11.4 ms | 1,140 km | San Jose | Cupertino | No |
| 185.199.108.153 | GitHub Pages | 51.1 ms | 5,109 km | San Francisco | Oakland City | No |
| 140.82.112.3 | github.com | 227.5 ms | 22,749 km | San Francisco | San Francisco | Yes |
A few things jump out.
Country was never the problem. 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.
City agreement was a coin flip. 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.
github.com was the only claim that survived. At 227 ms the physical limit stretches around the planet, so the check can't rule anything out. Every other row failed it.
Why the answers are "wrong" (and why that's not quite the right word)
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.
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:
$ curl "https://ipcomet.com/api/whois?q=1.1.1.1"
{"kind":"ip","query":"1.1.1.1","name":"APNIC-LABS","cidr":"1.1.1.0/24",
"type":"ASSIGNED PORTABLE","country":"AU","organization":"APNICRANDNET Infrastructure Contact", ...}
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.
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.
What I'd take from this
- Country-level answers are fine for most things. Everything agreed on it. It still isn't a legal boundary, so don't build compliance on a free database.
- Treat the city as a hint. Two decent sources disagreed on half of a small, famous sample. Don't show a user "Berkeley" as a fact.
- For anycast, CDN and DNS addresses, ignore the location. It tells you who owns the block and nothing about where the server is.
- A ping is a cheap lie detector. If you ever need to sanity-check a location claim (a proxy's advertised country, say), the check below takes a few lines.
The script
This is a trimmed version of what I ran: one IP in, a verdict out. It uses only the standard library plus ping, so it works on macOS and Linux.
import json, math, re, subprocess, sys, urllib.request
def get(url):
req = urllib.request.Request(url, headers={"User-Agent": "geo-check/1.0"})
return json.load(urllib.request.urlopen(req, timeout=15))
def km(a, b): # great-circle distance (haversine)
(la1, lo1), (la2, lo2) = [(math.radians(x), math.radians(y)) for x, y in (a, b)]
h = math.sin((la2 - la1) / 2) ** 2 + math.cos(la1) * math.cos(la2) * math.sin((lo2 - lo1) / 2) ** 2
return 2 * 6371 * math.asin(math.sqrt(h))
def best_rtt_ms(ip, count=10):
out = subprocess.run(["ping", "-c", str(count), "-q", ip], capture_output=True, text=True).stdout
return float(re.search(r"= ([\d.]+)/", out).group(1)) # first number is the minimum
ip = sys.argv[1]
me = get("https://ipcomet.com/api/me")
target = get(f"https://ipcomet.com/api/lookup?q={ip}")
if "latitude" not in target: # ipcomet omits fields it can't determine
sys.exit("no coordinates for this IP")
claimed = km((me["latitude"], me["longitude"]), (target["latitude"], target["longitude"]))
rtt = best_rtt_ms(ip)
limit = rtt / 2 * 200 # light in fibre: ~200 km per ms, one way
print(f'{ip}: database says {target.get("city")}, {target.get("country")}')
print(f"claimed distance : {claimed:,.0f} km")
print(f"best round trip : {rtt:.1f} ms -> physical limit {limit:,.0f} km")
print("verdict :", "IMPOSSIBLE" if claimed > limit else "plausible")
Run it with python3 geo_check.py 8.8.8.8. This is what it printed for me, on a bad minute for my connection:
8.8.8.8: database says Mountain View, United States
claimed distance : 12,195 km
best round trip : 55.8 ms -> physical limit 5,582 km
verdict : IMPOSSIBLE
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.
Two bugs bit me while I built the full version, and they're worth knowing about. geojs.io returns the string "nil" for coordinates it doesn't have instead of leaving the field out, so a plain float() 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.
About ipcomet's numbers
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.
The full endpoint list, with rate limits, is at ipcomet.com/developers.
I maintain ipcomet, 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.
Top comments (0)