If you still think of “nginx GeoIP” as tucking a GeoLite2-Country.dat file onto the disk and leaving a geoip_country line in the config, 2026 has a surprise in store: that format is gone from the current module, and the replacement is cleaner. In nginx 1.31.7 (mainline, 14 October 2026) the ngx_http_geoip_module and ngx_stream_geoip_module now read databases in the MaxMind DB (MMDB) format directly - the same format that MaxMind has shipped as the default for its GeoIP2 and GeoLite2 product lines for years.[1] No wrapper, no third-party dynamic module needed for the basic case.

Complementing our earlier guide on blocking AI scrapers with nginx, which handles the user-agent side of the bot fight, this guide covers the other axis: where a request comes from. Country-level blocking and geo routing are how you reject bot load that never identifies itself, comply with jurisdiction rules, and send users to the right regional endpoint.

Why geo-block in 2026

Geolocation at the country level answers problems that user-agent matching cannot, and in 2026 the three reasons land differently than they did a decade ago.

Filtering AI-scraper and bot load by source country. Many AI-crawler fleets and scraping operations run from a small set of cloud and hosting networks concentrated in a few countries. If your analytics show an overwhelming share of abusive traffic from networks you will not legitimate-serve anyway, a country block at the edge drops that load before it touches your app - which is considerably cheaper than rate-limiting thousands of individual IPs. A return 444 (or a geo-aware rate limit) measured by source country complements the AI-scraper user-agent defense because the scrape traffic that spoofs a browser UA still comes from those networks.

Compliance (GDPR and jurisdiction rules). Sometimes “who may access” is a legal question, not a technical one. GDPR and sector-specific rules can require restricting content to specific territories, or can motivate refusing to process data from jurisdictions you do not have agreements with. Country-level routing is the blunt but effective tool: the data literally does not reach your application.

Regional spam and brute force. Sign-in brute force, credential-stuffing and comment spam are heavily regionalized. Blocking entire countries for specific endpoints - or throttling them harder than everyone else - kills a large fraction of that noise at the edge without touching the healthy traffic.

The honest caveat up front: geo-blocking is not a security boundary. Geolocation is based on registered IP ranges, can be evaded with a VPN or an IP rented elsewhere, and MaxMind itself is explicit that IP geolocation is imprecise.[2] Treat it as a traffic-shaping and surface-reduction tool, not an access-control guarantee.

What changed: .dat is dead, only MMDB remains

The old ngx_http_geoip_module read databases in the GeoIP legacy DAT format through the separate MaxMind GeoIP Legacy C library. That is the .dat/.dat.bak pair you may remember. The module has supported MMDB since 1.31.7, on top of the legacy DAT support, and the documentation now shows geoip_country GeoLite2-Country.mmdb; as the example configuration.[3]

The difference between the formats matters for what you download:

  • DAT (GeoIP legacy) was the format of the old GeoLite / GeoIP databases. It required the GeoIP Legacy C library (geoip-api-c) and is the format that MaxMind has effectively retired from current downloads.
  • MMDB (MaxMind DB) is the format of GeoIP2 and GeoLite2 databases - everything MaxMind distributes today. It is a custom binary format “designed to facilitate fast lookups of IP addresses,” the spec is open and published, and it is read by the libmaxminddb C library.[4][5] Dynamic-language bindings for the same format power the GeoIP2 client libraries.

Concretely for 2026: the GeoLite2 databases you download from your MaxMind account are delivered as .mmdb (binary, for fast lookups at scale) or .csv (for import into a database).[6] The .dat files that the old GeoIP C API expected are no longer the product. So the practical migration is: stop looking for .dat, start pointing your config at the .mmdb file.

Prerequisite: libmaxminddb

For MMDB databases the geoip module requires the libmaxminddb library at build time; the legacy DAT databases instead need the GeoIP Legacy C library.[3] If you are using a distro package that already enables --with-http_geoip_module (or the stream variant), you need the corresponding runtime - on Ubuntu/Debian, MaxMind ships a PPA with libmaxminddb0, libmaxminddb-dev and mmdb-bin.[5] The module itself is not compiled in by default; on source builds it is enabled with --with-http_geoip_module and --with-stream_geoip_module.[3][7]

# Ubuntu/Debian via the MaxMind PPA
sudo add-apt-repository ppa:maxmind/ppa
sudo apt update
sudo apt install libmaxminddb0 libmaxminddb-dev mmdb-bin

MMDB lookup is fast - that is the entire point of the format. But as with any “is it fast” claim, we do not publish a number we have not measured. What the docs do guarantee is the format’s design goal: “fast lookups of IP addresses.” Absent our own benchmark on a live account, we leave the figure out rather than invent one.

Get the database: GeoLite2 requires an account, and it must stay fresh

Free GeoLite2 databases require a MaxMind account and a license key - MaxMind is explicit that you sign up, then generate a license key that authenticates downloads and web-service requests.[8] You will not find a naked direct .mmdb link that works without credentials.

Two constraints from MaxMind shape your update loop:

  • Download limit: GeoLite users are limited to 30 database downloads per day.[6]
  • Freshness is a license condition: the GeoLite EULA requires you to keep data up to date and delete GeoLite databases within 30 days of a new release.[6] On top of the legal requirement, a stale country database is a liability: an IP that moved networks resolves to the wrong country, and your block misroutes real users.

The recommended way to keep a binary database current is the GeoIP Update program (geoipupdate), which performs automatic updates for GeoIP and GeoLite binary databases.[9] Configure its GeoIP.conf with your AccountID and LicenseKey and run it from cron; it is Apache-2.0/MIT and installable from the PPA or a tarball.[10]

# /etc/GeoIP.conf
AccountID 123456
LicenseKey YOUR_LICENSE_KEY
EditionIDs GeoLite2-Country GeoLite2-ASN

# Place the databases where your nginx config expects them
# (the distro packages default to /usr/share/GeoIP; a /usr/local build uses /usr/local/share/GeoIP)

Wire it up: the 1.31.7 module

With a current GeoLite2-Country.mmdb on disk, the module is two lines in the http context:

http {
    geoip_country  /usr/share/GeoIP/GeoLite2-Country.mmdb;
    geoip_city     /usr/share/GeoIP/GeoLite2-City.mmdb;   # optional
    geoip_org      /usr/share/GeoIP/GeoLite2-ASN.mmdb;    # optional
}

That is it - nginx 1.31.7+ reads the MMDB natively. The country database alone gives you these variables, documented on the module page:[3]

  • $geoip_country_code - two-letter code, e.g. RU, US.
  • $geoip_country_code3 - three-letter code, e.g. RUS, USA.
  • $geoip_country_name - country name, e.g. Russian Federation, United States.

The city database adds region, city, postal code, latitude/longitude and continent-code variables ($geoip_city_country_code, $geoip_region, $geoip_city, …); the ASN database adds $geoip_org.[3] If you are behind a proxy, geoip_proxy and geoip_proxy_recursive tell nginx to trust X-Forwarded-For from specific proxies - without them the module geolocates your proxy’s IP, not the client’s.[3]

A name worth getting right. You will see tutorials using a geoip2 { } block with auto_reload and $geoip2_data_country_code. That is a different thing - the popular third-party ngx_http_geoip2_module by leev, which binds MMDB variables through a geoip2 directive.[11] The stock nginx module in 1.31.7 is the native one above: geoip_country/geoip_city/geoip_org and $geoip_country_code. Both produce MMDB-backed lookup; the leev module additionally offers auto_reload and custom variable naming, at the cost of building it as a dynamic module and maintaining it yourself. If you want the stock build, use the native directives.

Block and route by country: map, if, and the geo-aware rate limit

The clean stock-nginx way to translate a country variable into an action is a map. Put the decision in one place, then use it everywhere:

# http context
geoip_country /usr/share/GeoIP/GeoLite2-Country.mmdb;

# Geo routing: send EU users to a regional endpoint
map $geoip_country_code $region_backend {
    default  global.example.com;
    DE       eu.example.com;
    FR       eu.example.com;
    NL       eu.example.com;
}

If you are tempted by an allowlist map - enumerating the countries you serve and defaulting everything else to “block” - resist it: a map resolves an exact string match first, then the longest wildcard, then the first matching regexp, so a blocklist’s default 0 is doing exactly one job, while an allowlist means maintaining the full list of countries you serve as your audience grows. A blocklist is easier to reason about:

map $geoip_country_code $is_blocked_country {
    default   0;
    KP        1;   # North Korea
    IR        1;
    RU        1;   # example only - pick your own list
    CN        1;
}

Then act on it in a server block. The if + return 444 combination reuses the trick from the AI-scraper guide: 444 closes the connection without a response, which is cheaper than a real 403 and ideal for bot rejections:

server {
    listen 443 ssl;
    server_name example.com;

    if ($is_blocked_country) {
        return 444;
    }

    location / {
        proxy_pass http://backend;
    }
}

For geo routing, put the mapped $region_backend into the proxy target. map gives you a variable; proxy_pass consumes it:

location / {
    proxy_pass http://$region_backend;
}

Geo-aware rate limiting combines the map with ngx_http_limit_req_module: key the zone on a country-derived token so whole regions get one bucket, while your good regions stay free. The trick is to emit the key only for the countries you want to throttle - requests with an empty key are not accounted at all, so a “0 for everyone else” flag would put your entire good traffic into one shared bucket, which is the opposite of what you want:[12]

# http context
map $geoip_country_code $throttled_country {
    default  "";     # empty key -> the request is not accounted
    KP       KP;
    IR       IR;
    CN       CN;
}

limit_req_zone $binary_remote_addr zone=perip:10m rate=5r/s;
limit_req_zone $throttled_country   zone=percountry:10m rate=1r/s;

# server context, alongside the block:
location /signin {                     # a brute-force target
    limit_req zone=perip      burst=20 nodelay;
    limit_req zone=percountry burst=3  nodelay;
    limit_req_status 429;
}

The leaky-bucket semantics, burst, nodelay and zone-sizing notes are the same as in the AI-scraper guide - one megabyte holds roughly 8 thousand 128-byte states on 64-bit platforms, one per distinct key.[12]

The stream module for TCP/UDP

Geolocation is not HTTP-only. The ngx_stream_geoip_module (enabled with --with-stream_geoip_module, present since 1.11.3) creates the same country/city/org variables from the client IP, and per the 1.31.7 changelog it too supports MMDB alongside the legacy format.[7] Use it in a stream { } block to geo-block or geo-route non-HTTP traffic - SSH, an SMTP relay, a database port you only want reachable from your own region:

stream {
    geoip_country /usr/share/GeoIP/GeoLite2-Country.mmdb;

    map $geoip_country_code $deny_mysql {
        default 0;
        IR      1;
        KP      1;
    }

    server {
        listen 3306;
        if ($deny_mysql) {
            return;
        }
        proxy_pass db.internal:3306;
    }
}

In stream, return; (no code) closes the TCP connection, which is the stream-equivalent of dropping the request. As with any if, keep the condition minimal and use map for the lookup - the stream module’s if has the same constraints as the http module’s if (it works, but you cannot rely on full rewrite-module semantics inside it).

Limitations and operational notes

  • MMDB is not a drop-in for .dat syntax. If you are migrating an old config that used the legacy GeoIP variables and the GeoIP Legacy C library, the variable names the module exposes ($geoip_country_code, etc.) carry across, but the database pipeline does not: you must obtain and update an .mmdb file, which requires a MaxMind account and license key for GeoLite2.[8] Old .dat files from a third-party source are unsupported.
  • No auto_reload in the stock module. The native geoip_country reads the file; on nginx 1.31.7 there is no built-in reload-on-change for it the way the leev module’s auto_reload provides.[3][11] Either restart/reload nginx after geoipupdate runs, or use the third-party module if hot reload is a hard requirement. This is exactly why you pair geoipupdate with a systemd timer or a reload step after the download.
  • License and freshness are real obligations. With GeoLite2 you accept the download limit (30/day) and the delete-within-30-days rule.[6] If you outgrow GeoLite2, paid GeoIP databases add data the free tier does not include - ISP name, connection type, domain, user type, and separate anonymous-IP databases for proxy/VPN detection - but that is a paid upgrade, and we link to it only as the official path, citing MaxMind’s own docs.[13]
  • Accuracy is not a guarantee. MaxMind states that geolocation locations are often near population centers and should not identify a specific address; the free GeoLite City is “considerably less accurate” than paid GeoIP City data and not recommended for commercial use.[2][6] For country-level blocking of bot networks this granularity is usually sufficient - a hosting network in Iran or North Korea geolocates clearly. It is not a substitute for IP-allowlist security on anything sensitive.

The short version

Nginx 1.31.7+ reads GeoLite2 MMDB files natively through geoip_country / geoip_city / geoip_org, so the .dat + GeoIP Legacy C library pipeline is obsolete. Your 2026 setup: install libmaxminddb, download GeoLite2-Country.mmdb with a MaxMind account and license key, point the directive at it, translate $geoip_country_code into an action with map (return 444 for a blocklist), and reuse that variable both for geo routing and for geo-aware rate limits. The stream module extends the same idea to TCP/UDP ports. Keep the DB current with geoipupdate in cron - freshness is both a license condition and what keeps your block accurate - and remember there is no stock auto_reload, so reload nginx after updates. Geo-blocking is a traffic-shaping layer, not an access boundary: it reduces surface and bot load, but a VPN defeats a country check, so pair it with real per-endpoint hardening.

Sources

  1. nginx - CHANGES 1.31.7 (14 Oct 2026): “Feature: the ngx_http_geoip_module and ngx_stream_geoip_module now support databases in the MaxMind DB (MMDB) format.” https://nginx.org/en/CHANGES - checked 2026-10-18
  2. MaxMind - GeoLite Databases and Web Services (“IP geolocation is inherently imprecise… locations are often near population centers… should not be used to identify a particular address or household”). https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/ - checked 2026-10-18
  3. nginx - Module ngx_http_geoip_module (“Since 1.31.7, the module supports databases in MaxMind DB (MMDB) format”; requires libmaxminddb for MMDB; directives geoip_country/geoip_city/geoip_org/geoip_proxy/geoip_proxy_recursive; variables $geoip_country_code/$geoip_country_code3/$geoip_country_name etc.; --with-http_geoip_module; example geoip_country GeoLite2-Country.mmdb;). https://nginx.org/en/docs/http/ngx_http_geoip_module.html - checked 2026-10-18
  4. maxmind/libmaxminddb - “C library for the MaxMind DB file format… custom binary format designed to facilitate fast lookups of IP addresses… The MaxMind DB format is an open format.” https://github.com/maxmind/libmaxminddb - checked 2026-10-18
  5. MaxMind DB spec - open format spec. https://maxmind.github.io/MaxMind-DB/ - checked 2026-10-18
  6. MaxMind - GeoLite databases (“two formats: binary (.mmdb)… and .csv”; GeoLite users limited to 30 database downloads per day; EULA requires deleting GeoLite databases within 30 days of a new release; “The free GeoLite City is considerably less accurate than the paid GeoIP City data, and is not recommended for commercial use cases”). https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/ - checked 2026-10-18
  7. nginx - CHANGES 1.31.7 (module name ngx_stream_geoip_module) and Module ngx_stream_geoip_module (1.11.3+, --with-stream_geoip_module, country/city/org variables from client IP). https://nginx.org/en/docs/stream/ngx_stream_geoip_module.html - checked 2026-10-18
  8. MaxMind - GeoLite: “After you have a MaxMind account, you should generate one or more license keys… to authenticate database downloads or web service requests.” https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/ - checked 2026-10-18
  9. MaxMind - Updating GeoIP and GeoLite Databases (“For clients using our binary database format, we highly recommend using GeoIP Update, but the direct download method is available if necessary”; cron example). https://dev.maxmind.com/geoip/updating-databases/ - checked 2026-10-18
  10. maxmind/geoipupdate - automatic updates of GeoIP and GeoLite binary databases; config /etc/GeoIP.conf (or /usr/local/etc/GeoIP.conf) with AccountID/LicenseKey; Apache-2.0 or MIT; PPA on Ubuntu. https://github.com/maxmind/geoipupdate - checked 2026-10-18
  11. leev/ngx_http_geoip2_module - third-party module exposing the geoip2 { auto_reload …; $geoip2_data_country_code …; } syntax; BSD 2-Clause; requires libmaxminddb; supports http and stream. https://github.com/leev/ngx_http_geoip2_module - checked 2026-10-18
  12. nginx - Module ngx_http_limit_req_module (leaky bucket, burst, nodelay, limit_req_status default 503, zone size math: ~8 thousand 128-byte states per MB on 64-bit). https://nginx.org/en/docs/http/ngx_http_limit_req_module.html - checked 2026-10-18
  13. MaxMind - GeoIP databases (paid tier: ISP, Connection Type, Domain, Anonymous IP databases for proxy/VPN detection, ASN; GeoLite counterpart pages linked from the same index). https://dev.maxmind.com/geoip/docs/databases/ - checked 2026-10-18