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.