# geoip-lite installs a stale database and never tells you

> A geolocation lookup library written entirely in JavaScript with no native build step, at the cost of holding the whole database in memory. The operational consequence is in the first note of the readme: the data cannot ship current with the package, so a fresh install gives you a library that returns confident answers from an out-of-date database.

**geoip-lite/node-geoip** — Native NodeJS implementation of MaxMind's GeoIP API -- works in node 0.6.3 and above, ask me about other versions

- Repository: https://github.com/geoip-lite/node-geoip
- Website: https://npmjs.org/package/geoip-lite
- Stars: 2,425 · Forks: 359
- Language: JavaScript
- License: NOASSERTION
- Published: 2026-09-28 · Updated: 2026-09-28 · Language: en
- Canonical page: https://hysenlabs.com/projects/geoip-lite-node-geoip

## The repository description advertises a runtime the package refuses to install on

The repository description says this is a native implementation of the vendor's geolocation API that works in Node 0.6.3 and above, and invites readers to ask about other versions.

The package manifest declares a hard engine floor of Node 24.0.0 or newer, and the readme's requirements section states the same number in isolation under a heading of its own.

So the one-line summary a search result shows you advertises a runtime from 2011, and the package manager will refuse to install the package on anything released since. The two facts are separated by a decade and a half of Node versions.

The invitation to ask about other versions compounds it. It reads as though support for older runtimes is a matter of preference to be negotiated, when in fact the manifest has already answered the question and the answer is no.

This is what happens to a repository description: it is written once and never revisited, while the readme and the manifest are both current. Here the drift is unusually consequential, because the description is what a search engine shows, so someone can decide against the package from a string that no longer describes it.

The fix is one edit. The fact that it has survived this long suggests nobody reads their own repository description.

## The package installs a stale database, and the API has no way to say so

The first note in the readme is in capitals and says you must update the data files after installation, because the vendor's licence does not permit distributing the current version of the data files with the package.

That is a licence constraint, not an oversight, and the library works around it by shipping a converter and an update script instead of the data. Which means every installation begins in a known-wrong state and has to be repaired.

The repair is not optional in practice. The update script needs an account with the data vendor and a licence key, needs network access, and is subject to that vendor's download rate limits. Running it looks like this:

```shell
#update data if new data is available
npm run-script updatedb license_key=YOUR_LICENSE_KEY
```

What is not stated anywhere is how a caller knows whether the repair happened. The lookup function's documented return value is an object of ranges, country, region, a European Union membership flag, a timezone, a city, coordinates, a metro code, and an area code. There is no data-build timestamp, no version field, and no freshness indicator.

So a stale database does not produce nulls. It produces plausible, confident, wrong answers, including a city and a timezone for an address that has since been reassigned. For anything that logs, bills, routes, or displays a location, that is the failure mode you most want to avoid and the one the interface gives you no way to detect.

## Everything lives in memory, on purpose, and there are no plans to change it

The readme is blunt about the memory cost and about the fact that it is not going to be fixed.

It says the package requires a lot of memory, that it is known to fail on a specific named class of cheap virtual machine, and that there are no plans to change this, because the package stores all data in RAM in order to be fast. Those are four consecutive sentences stating the constraint, naming where it breaks, and declining to address it.

The design is coherent. Everything at runtime is in-memory, there are no callbacks, and all blocking file input happens once at startup, so a lookup after that is pure computation. The readme puts a ceiling on the cost: startup may take up to two hundred milliseconds while the data is read in and indexed.

What the design does not give you is a bound. Memory grows with the size of the upstream database, which grows as address space is allocated and reallocated, and nothing in the interface lets you shrink what you load. On a small instance the documented failure mode is not an error you catch; it is the process failing to start.

The philosophy section explains why the design is this way, and the explanation dates it. The stated motivator was how hard it was to build the vendor's C library on a particular operating system without going through a third-party package manager. Every subsequent decision, the all-in-RAM store, the custom binary format, the zero native dependencies, follows from avoiding that one build problem.

## Five runtime dependencies and four different ways of pinning them

The runtime dependency list is five entries, and no two are pinned the same way.

One is pinned to a single exact version with no range at all, which means it will never float. One is a range written with a hyphen that spans exactly three patch releases and nothing outside them. One is a range written with a hyphen that spans from a patch release of one minor version to a minor version several releases later, crossing more than one minor boundary. Two use the caret form, which allows anything below the next major version.

So a fresh install of this package resolves four different upgrade policies at once. The exact pin will age without moving. The three-patch range will move whenever the vendor publishes a patch. The wide hyphen range can jump a minor version on a routine release. The two carets can jump a major version, which for a terminal-colour library is where breaking changes live.

That last one is worth pausing on. The package that renders coloured terminal output is the one allowed to upgrade its major version without a lockfile update, and the package that is a lazy-evaluation helper is frozen. Neither is obviously the right way round.

Nothing here is broken. It is a dependency list that grew by accretion, with each line added by whoever needed it that day and in whatever style the tool in front of them produced.

## The on-disk format is the package's own, which is why the updater cannot be optional

The introduction explains the design in a few sentences, and one of them is load bearing.

The data vendor provides data files plus open source libraries that read them. The usual approach is to write a binding to the vendor's C interface for each language. This package instead converts the vendor's CSV files into its own binary format, and the readme explicitly notes that this is different from the binary format the vendor provides.

That decision explains the whole package. Because the on-disk format is bespoke, you cannot point this library at the vendor's own database files. The converter is the only path, and therefore the update script is mandatory rather than a convenience. There is no configuration option that says use the vendor's files instead, because there is no reader for them.

It also explains the memory profile. The input is CSV, which is a considerably larger thing to hold than a packed binary database, and a converter that streams it into a compact format is doing real work during the update. That is why the update is a script with a debug flag and a force flag rather than a file download.

The one convenience that follows is that the custom format is internal, so it can change without a major version bump. The readme says so about the range values in the return object, which it describes as internal and gives a separate method for turning them into something readable.

## IPv6 gives you a country and nothing below it

Both address families are supported. What differs is how much you get back.

The readme states that the vendor's IPv6 database does not currently contain city or region information, and that city, region, and postal code lookups are therefore only supported for IPv4.

That is upstream, and it is not going to change on this library's schedule. What matters is how it presents. The lookup returns the same object shape either way, and the documented structure includes a city, a timezone, coordinates, a metro code, and an area code. There is no flag saying those fields are unavailable for the family you asked about.

So for anyone doing city-level analytics, the failure is an object with missing or empty fields rather than an error. Depending on the consumer, that can average out into a plausible-looking number, appear as an empty string in a report, or throw much later in a pipeline that assumed a string.

The address parsing side is more forgiving. The lookup accepts dotted quad notation, colon notation, and a 32-bit unsigned integer treated as IPv4, and it asks you to strip brackets around an IPv6 address before passing it in. That last one is a small trap: bracketed form is what URLs use, so a value taken straight out of a request header will need cleaning first.

## A sub-microsecond figure with no method, and a legacy CI config in the tree

The readme makes a performance claim in the section explaining why the package exists rather than its competitor.

It says the package is not as fully featured as bindings that use the vendor's native library, and that by reducing scope it is significantly faster at lookups, giving an average IPv4 figure under half a microsecond and an IPv6 figure of about one and a bit microseconds.

There is no hardware, no Node version, no sample size, and no method. The word average implies a benchmark, and none is described or linked. The figure also predates the interpreter floor by many releases: the description still advertises a runtime from 2011, so this number was measured under conditions that no longer apply to the package's installable audience.

The comparison it makes is fair in shape. Doing less work per lookup is a legitimate reason to be faster, and a smaller feature surface is a real trade. It is the precision of the number that does not survive contact with the rest of the repository.

Two smaller signals point the same way. The root directory still carries a continuous integration configuration for a service that is no longer where new projects run, alongside the current automation directory, and the package manifest carries a top-level configuration block that modern installers ignore. Both are leftovers from a period when this was a different project, kept because removing them was nobody's job.

## Conclusion

This is a good library with one trap that a reader has to be told about explicitly. Decide how you are going to keep the data current before you ship, because the failure mode is silent: the lookup function returns a populated object built from whatever data file is on disk, and nothing in its return value says how old that file is. Schedule the update, verify that it succeeded, and treat a missing or failed update as an outage rather than as a lookup returning nothing. Also budget for the memory, since the design is committed to it, and remember that IPv6 gives you a country and nothing below it.

## FAQ

### What is geoip-lite?

A geolocation lookup library for Node written entirely in JavaScript with no native build step. It returns country, region, European Union membership, timezone, city, coordinates, a metro code and an area code for an IP address, and it is completely synchronous, with all file input happening once at startup.

### Does geoip-lite include the geolocation database?

It includes the data files, but they cannot be current. The data vendor's licence does not permit distributing the latest version with the package, so the readme's opening note says you must run an update after installation, using an account and licence key from that vendor and subject to its download rate limits.

### How can I tell whether my geoip-lite data is current?

The documented return value has no data version, build timestamp, or freshness field. A lookup against stale data returns a populated object rather than null, so the caller has no signal from the API and needs to verify the update step separately.

### What Node version does geoip-lite require?

Node 24.0.0 or newer, declared as a hard engine floor in the package manifest and stated in the requirements section of the readme. The repository description still advertises support from Node 0.6.3, which is stale.

### Can geoip-lite return city data for an IPv6 address?

No. The vendor's IPv6 database does not contain city or region information, so city, region, and postal code lookups are supported only for IPv4. The return object keeps the same shape, so the missing fields are not flagged as unavailable.

## Sources

- [geoip-lite/node-geoip on GitHub](https://github.com/geoip-lite/node-geoip)
- [Issues](https://github.com/geoip-lite/node-geoip/issues)
- [Project website](https://npmjs.org/package/geoip-lite)
- [README](https://github.com/geoip-lite/node-geoip/blob/main/README.md)
- [Releases](https://github.com/geoip-lite/node-geoip/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/geoip-lite-node-geoip
