DEV Community

Cover image for I Used 5 Free IP Geolocation APIs — Here's What I Learned
Onizuka
Onizuka

Posted on • Edited on

I Used 5 Free IP Geolocation APIs — Here's What I Learned

I Compared 5 Free IP Geolocation APIs on VPN, Tor, and Datacenter IPs

Earlier this year someone created about forty accounts on my SaaS in a single night, all of them geolocating to Berlin. They weren't in Berlin. It was a Tor exit node, and my signup check — a free IP API I'd copied out of a Stack Overflow answer — passed every one of them. I noticed the next morning, when the Mailgun overage email arrived, and spent the rest of the week repairing sender reputation.

After that I stopped trusting the check and ran a proper comparison: five free IP geolocation APIs, the same five test IPs, 50 calls each from the same VPS.

The five APIs

  • ip-api.com: the one everyone pastes into tutorials
  • ipapi.co: clean JSON, popular with frontend devs
  • ipinfo.io: the "serious" one with the nice dashboard
  • ipwho.is: newer, generous free tier
  • IP Geolocation API on RapidAPI: advertises VPN/proxy/Tor detection and a fraud score on the free plan

Setup

I ran everything from a Frankfurt VPS against five IPs: my home connection, a NordVPN exit, a Tor exit node from the public list, an AWS EC2 instance, and Cloudflare WARP. The harness, with keys removed:

import time, statistics, requests

APIS = {
    "ip-api":   ("http://ip-api.com/json/{ip}?fields=status,country,city,proxy,hosting", None),
    "ipapi.co": ("https://ipapi.co/{ip}/json/", None),
    "ipinfo":   ("https://ipinfo.io/{ip}/json?token=TOKEN", None),
    "ipwho.is": ("http://ipwho.is/{ip}", None),
    "geo-api":  ("https://ip-geolocation-api1.p.rapidapi.com/json/{ip}",
                 {"X-RapidAPI-Key": "KEY",
                  "X-RapidAPI-Host": "ip-geolocation-api1.p.rapidapi.com"}),
}

TEST_IPS = ["home", "nordvpn", "tor-exit", "ec2", "warp"]  # real IPs redacted

def bench(name, url_tpl, headers):
    lat, out = [], {}
    for ip in TEST_IPS:
        t = time.perf_counter()
        r = requests.get(url_tpl.format(ip=ip), headers=headers, timeout=5)
        lat.append((time.perf_counter() - t) * 1000)
        out[ip] = r.json()
    print(name, f"median {statistics.median(lat):.0f} ms")
    return out
Enter fullscreen mode Exit fullscreen mode

If you rerun this, your latencies will differ depending on where your box sits. The detection results shouldn't.

Results

Median latency over the 50 calls per API:

API Median latency Free tier HTTPS free? VPN/Tor flag free?
ip-api.com 88 ms 45 req/min No Partial (proxy/hosting)
ipapi.co 214 ms 1,000 req/day Yes No
ipinfo.io 131 ms 50k req/month Yes Paid only
ipwho.is 176 ms 10k req/month Yes No
IP Geolocation API 152 ms RapidAPI free tier Yes Yes, plus fraud score

ip-api.com is noticeably faster than the rest, the only one under 100 ms from my box. But the last column is the one that matters for my use case: four of the five either can't flag a Tor exit at all or only do it on a paid plan.

City accuracy was closer. Four of the five put my home IP in the right city; ipapi.co was off by about 60 km. All five agreed on the EC2 instance, but datacenters are the easy case. Residential addresses are where the databases disagree.

Where the free tiers got me

ip-api.com's free tier is HTTP only , TLS requires paying. My site is HTTPS, browsers block mixed content, and my frontend lookup failed in production for roughly three weeks before I noticed. It never failed in dev, because localhost doesn't enforce mixed content. The deeper bug was mine: my error handler defaulted to allowing the signup when the lookup failed.

Rate limits were the second issue. 45 requests per minute sounds like a lot until you get posted somewhere with an audience. I hit 429s during one spike, and every failed lookup was another unscreened signup. If the lookup sits in your signup path, cache aggressively and decide explicitly what happens on failure. The default will be wrong.

A smaller one: testing from localhost. request.remote_addr gives you 127.0.0.1, and most of these APIs return nulls. ipapi.co returns the string "Reserved", which my code stored as a country for a while. Filter private ranges before you call anything.

VPN and Tor detection

This is the part that cost me money, so I tested it the hardest.

The Tor exit node: ip-api flagged hosting: true but proxy: false, which isn't wrong but isn't actionable either. ipapi.co and ipwho.is returned plain geodata with no risk fields. ipinfo's free response doesn't include its privacy detection fields. The IP Geolocation API returned vpn: false, tor: true, proxy: true, and a fraud score of 87.

The NordVPN exit was similar , only two of the five surfaced anything useful.

The score turned out to be more useful than the boolean. A bare is_proxy forces a block/allow decision you may not want to make. A number lets you do "above 70, require email verification; above 90, reject," which is what I run now.

Using it

Grab a key on the RapidAPI listing, then:

curl -X GET "https://ip-geolocation-api1.p.rapidapi.com/json/8.8.8.8" \
  -H "X-RapidAPI-Key: YOUR_KEY" \
  -H "X-RapidAPI-Host: ip-geolocation-api1.p.rapidapi.com"
Enter fullscreen mode Exit fullscreen mode

And the handler my signups go through now:

import ipaddress
import requests

RAPID_KEY = "YOUR_KEY"
URL = "https://ip-geolocation-api1.p.rapidapi.com/json/{}"

def check_ip(ip: str) -> dict:
    # Skip private/reserved ranges or you'll bill yourself for localhost
    if ipaddress.ip_address(ip).is_private:
        return {"allow": True, "reason": "private ip"}

    r = requests.get(
        URL.format(ip),
        headers={"X-RapidAPI-Key": RAPID_KEY,
                 "X-RapidAPI-Host": "ip-geolocation-api1.p.rapidapi.com"},
        timeout=3,
    )
    r.raise_for_status()
    d = r.json()

    score = d.get("fraud_score", 0)
    if d.get("tor") or score >= 90:
        return {"allow": False, "reason": "blocked", "score": score}
    if d.get("vpn") or d.get("proxy") or score >= 70:
        return {"allow": True, "reason": "require email verification", "score": score}
    return {"allow": True, "reason": "clean", "score": score}
Enter fullscreen mode Exit fullscreen mode

The details that matter: skip private IPs or you'll burn quota on localhost, keep the timeout short so geolocation can't hang a signup, and use raise_for_status so a 429 becomes an exception instead of a silently trusted None. Field names match the docs as of this writing; check the GitHub repo or the RapidAPI playground if something looks off.

What I'd pick

For a country code in a sidebar, ip-api.com is fine: fast, no key. Call it server-side so the HTTP-only free tier doesn't hit the mixed-content problem.

For gating signups or trials, you need the security fields, and ipinfo's privacy detection sits on a paid tier that's hard to justify for a side project. That's the gap the IP Geolocation API filled for me , geolocation plus VPN, proxy, and Tor flags and a fraud score, over HTTPS, on the free tier. It wasn't the fastest of the five and its city data was middle of the pack, but it's the only one that flagged the Tor exit.

My ranking for signup screening: IP Geolocation API, ip-api (the proxy flag is decent), ipinfo (good data, wrong tier for a side project), ipwho.is, ipapi.co. For plain country lookup, roughly reverse it.

The API is on RapidAPI and the GitHub repo has the docs. If you rerun the experiment, do it from your own region. These databases vary by geography, and Frankfurt results won't transfer to São Paulo.

One caveat: my 70/90 thresholds come from a couple of weeks of traffic, nothing more rigorous. If you gate signups or checkouts with IP data, I'd like to know what you threshold on , flags, a score, ASN type , and what false-positive rate you'll tolerate before loosening the rules.


Tags: #api #python #security #osint.

Which of these checks does your fraud flow actually run?

Top comments (0)