Moving IP Geolocation In-House: What We Learned

By — SDET & creator of findWhatIsMyIP.com · 2026-09-07

We just shipped a Bulk IP Lookup tool — paste up to 50 addresses, get the country, region, city and network (ASN) for each. Building it meant changing something under the hood that had been the same since the site started: where IP geolocation actually happens. This is the build-in-public write-up — why we moved it, what went wrong on the way, and what it bought us.

The old setup, and why it didn’t fit

Every location answer on the site — IP to Timezone, IP WHOIS, the "approximate location" line on a few pages — was resolved by calling a third-party API (ipinfo.io, with ip-api.com as a fallback) once per lookup, with a short in-process cache in front.

That is fine for one IP at a time. It falls apart for a bulk tool:

  • The free tiers are rate-limited (roughly 50k lookups a month, or 45 requests a minute). Fifty IPs in one paste, a few users at once, and you are into paid territory fast.
  • Every lookup is a round trip — 50–200 ms each. Fifty of them, even batched, is a slow page.
  • Every address a visitor pastes would be sent to two outside companies. For a site whose whole pitch is "here is what leaks about you," that felt wrong.

The fix is well known: run a local copy of the database. MaxMind’s GeoLite2 is the free version — a single binary file (.mmdb) you query in-process, no network call. We took GeoLite2-City (country, region, city, latitude/longitude, IANA time zone) and GeoLite2-ASN (the network operator). Together about 75 MB.

The challenges (the honest part)

1. It’s free, but it’s licensed

GeoLite2 needs a MaxMind account and a license key, and the database itself may not be redistributed. So the file cannot live in the git repo, and it cannot be baked into a public Docker image. The key has to exist on the server without ever touching a commit.

We landed on: the .mmdb files sit in a directory on the host, git-ignored; the container gets them through a read-only bind-mount; the license key lives in a chmod 600 file on the box that a small updater script reads from the environment. Nothing sensitive is ever staged, committed, or copied into an image layer.

2. Keeping it fresh with no cron infrastructure

IP allocations move around, so MaxMind rebuilds GeoLite2 twice a week and a stale database quietly gets things wrong. The site had no scheduled-job setup at all — every recurring task was run by hand.

Rather than pull in a job runner, we wrote a ~120-line update_geoip.py: it downloads each edition, verifies the SHA-256, and swaps the file into place atomically. The reader checks the file’s modification time on each request and reopens transparently when it changes, so a refresh needs no restart. The schedule is one documented crontab line. Boring, and boring is the point.

3. The obvious library was the wrong one

The standard Python package for this is geoip2. Installing it pulled in aiohttp and seven more async dependencies — all for a web-service client we would never call, since we only read a local file. We dropped down to maxminddb alone, which is the actual file reader, and handled MaxMind’s record schema directly. One dependency instead of nine.

4. A local database has no cost per lookup — which is the risk

With a third-party API, someone scripting your endpoint burns your quota, so it self-limits. A local database has no such ceiling, which is exactly why a bulk endpoint invites being used as a free geolocation API by someone else’s product. Our API is already gated to requests coming from our own pages, and on top of that we capped it: 50 addresses per request, 10 requests a minute, 120 an hour. Enough for a person pasting a list; not enough to build a business on.

5. Testing something you can’t call from the outside

Our internal APIs only answer requests that come from the site itself, so you can’t just curl them, and the CI environment has no licensed database. MaxMind publishes a set of tiny, synthetic test databases under an open license — a few known IPs mapped to known cities and ASNs. We committed those as fixtures, so the whole test suite runs green without the real data anywhere near it.

6. Making staleness visible

Because a stale geo database fails silently, we surface its build date right on the tool: "database updated 2026-09-04." If the weekly refresh ever stops, it shows.

What it bought us

  • Speed. A lookup went from a 50–200 ms network round trip to a sub-millisecond file read. Fifty at once is now instant.
  • No quota, no outage surface. The lookup can’t rate-limit us and can’t go down independently of the site.
  • Privacy. The addresses you paste into Bulk IP Lookup are matched against a file on our server and never sent anywhere. That is a claim we can actually stand behind now.
  • The tool exists at all. A 50-IP batch tool isn’t practical on a metered third-party free tier. Self-hosting is what made it a tool rather than a demo.

The trade-off we accepted

GeoLite2 is less precise than a paid API, especially below country level. Country is right the large majority of the time; city is an estimate, and a poor one for mobile networks, VPNs and cloud IPs. The free data also has no VPN/proxy flag. The tool says all of this plainly rather than implying a precision the data doesn’t have.

What’s next

Right now the local database backs only the new tool. The next step is to move the existing pages (IP to Timezone, IP WHOIS) onto it as well, keeping a single third-party call only for the VPN/hosting signal that GeoLite2 doesn’t carry. Same latency and privacy win, wider reach.

Try it: Bulk IP Lookup · Related: WHOIS vs geolocation · IP to Timezone · Networking & security glossary

This product includes GeoLite2 data created by MaxMind, available from https://www.maxmind.com.

Awesome findWhatIsMyIP Blog