Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Use GeoIP2 once at the edge, make ordinary country decisions with NGINX map, reserve OpenResty Lua for policy that cannot be expressed statically, and include every response-varying dimension in your cache key. That combination blocks or routes traffic without putting a network lookup in every request, while preventing one country from receiving another country’s cached representation.

This guide shows a production-shaped configuration, the safety checks around it, and how to measure the trade-offs. IP geolocation is an estimate: VPNs, mobile carriers, proxies, and corporate egress can produce the wrong country. NGINX documentation does not establish a universal latency improvement, so measure your own traffic.

Choose the decision path before writing configuration

Keep the fast path deliberately boring. The edge should derive a normalized country or region code from a local MMDB file, then apply a native rule.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Requirement Best fit Why
Stable allow/deny list GeoIP2 plus map Configuration is easy to audit and does not execute application code per request.
Nearest regional upstream GeoIP2 plus an upstream-selection map Users can be routed to a regional server group; the actual latency benefit depends on your network and origin placement.
Signed policy, exceptions, or multiple signals OpenResty access_by_lua_block Lua can combine country, account state, an external policy feed, and request attributes.
Shareable API responses proxy_cache with a complete key Country, language, device, authorization state, and other representation dimensions remain isolated.

NGINX Plus has documented GeoIP2 dynamic-module packaging and API or key-value capabilities. Open-source NGINX and OpenResty are sufficient for many static country policies; choose Plus when its operational APIs or commercial support justify the license.

Prerequisites and data lifecycle

  • Install a GeoIP2 country or city MMDB database and keep its file path stable. A country database is enough for country blocking; use a city database only when you actually need region or city attributes.
  • Load the GeoIP2 dynamic module that matches your NGINX build. Keep the module, NGINX binary, and MMDB updates under the same change-control process.
  • Define policy separately from data freshness. A database update changes the location estimate; a policy update changes who is allowed; a cache purge changes what representation is served. Automate and monitor these three operations independently.
  • Decide what status communicates your policy. Use 403 for a forbidden request. Use 451 only when a legal or regulatory restriction is the reason and your organization has chosen that disclosure.

Implement fast country blocking with GeoIP2 and map

The following example allows the United States, Canada, the United Kingdom, and Germany. Replace the database path and country list with your policy.

load_module modules/ngx_http_geoip2_module.so;

http {
    geoip2 /etc/nginx/geoip/GeoIP2-Country.mmdb {
        $geo_country country iso_code;
    }

    map $geo_country $country_allowed {
        default 0;
        US 1;
        CA 1;
        GB 1;
        DE 1;
    }

    server {
        listen 443 ssl;
        server_name api.example.com;

        if ($country_allowed = 0) {
            return 403;
        }

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

map performs a constant-time configuration lookup and gives you a single, reviewable policy table. The if above contains only a return, which is the safe form for access rejection. If you need a different response body, route denied traffic to a named location instead of adding rewrite logic to the if.

Route countries to regional upstreams

Routing is not the same as blocking. Map countries to a regional group, then map that group to an upstream name.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
http {
    geoip2 /etc/nginx/geoip/GeoIP2-Country.mmdb {
        $geo_country country iso_code;
    }

    map $geo_country $region_group {
        default other;
        US na; CA na; MX na;
        GB eu; DE eu; FR eu;
        JP apac; SG apac; AU apac;
    }

    map $region_group $selected_upstream {
        default api_other;
        na api_na;
        eu api_eu;
        apac api_apac;
    }

    upstream api_na { server na-1.internal:8080; server na-2.internal:8080; }
    upstream api_eu { server eu-1.internal:8080; server eu-2.internal:8080; }
    upstream api_apac { server apac-1.internal:8080; server apac-2.internal:8080; }
    upstream api_other { server origin.internal:8080; }

    server {
        listen 443 ssl;
        server_name api.example.com;
        location / {
            proxy_pass http://$selected_upstream;
        }
    }
}

Use a region group only when every country in that group receives the same representation. If content, tax, inventory, or legal behavior differs by country, keep the country code as the cache dimension and, where necessary, as the routing dimension.

Use OpenResty Lua when static maps are not enough

OpenResty exposes the access phase through access_by_lua_block. It is appropriate for signed policy documents, customer-specific exceptions, time windows, or a decision that combines several request attributes.

lua_shared_dict policy 1m;

server {
    listen 443 ssl;
    server_name api.example.com;

    access_by_lua_block {
        local country = ngx.var.geo_country or ""
        local policy = ngx.shared.policy
        local blocked = policy:get("blocked:" .. country)

        if blocked == "1" then
            return ngx.exit(ngx.HTTP_FORBIDDEN)
        end
    }

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

Populate the shared dictionary from a refresh worker or another asynchronous mechanism. Do not perform an unbounded blocking network call in the access phase. Bound connection, read, and total policy-refresh times; retain the last known good policy when a refresh fails; and expose the policy version in logs so an incident can be correlated with a rollout.

Lua modules loaded with require are cached. Keep Lua code caching enabled in production: OpenResty’s reference documentation says, “Disabling the Lua code cache is strongly discouraged for production use and should only be used during development as it has a significant negative impact on overall performance.” With code caching enabled, source edits require an NGINX reload. Use a reload-safe deployment process and validate syntax before replacing workers.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Build an API cache that cannot cross country boundaries

Start with the origin’s cache policy. Honor Cache-Control, Vary, and related headers unless you have documented a deliberate override. Never share a response that is personalized, authenticated, unsafe, or otherwise non-shareable.

Separate cacheable methods and bypass conditions

http {
    proxy_cache_path /var/cache/nginx/api
        keys_zone=api_cache:100m
        max_size=10g
        inactive=10m
        use_temp_path=off;

    map $request_method $skip_method {
        default 1;
        GET 0;
        HEAD 0;
    }

    map $http_authorization $skip_auth {
        default 1;
        "" 0;
    }

    map $cookie_session $skip_session {
        default 1;
        "" 0;
    }

    geoip2 /etc/nginx/geoip/GeoIP2-Country.mmdb {
        $geo_country country iso_code;
    }

    map $geo_country $cache_geo {
        default other;
        US US;
        CA CA;
        GB GB;
        DE DE;
    }

    server {
        location /v1/catalog {
            proxy_cache api_cache;
            proxy_cache_methods GET HEAD;
            proxy_cache_bypass $skip_method $skip_auth $skip_session;
            proxy_no_cache $skip_method $skip_auth $skip_session;

            proxy_cache_key "$scheme|$host|$request_method|$uri|$args|$cache_geo|$http_accept_language";
            proxy_pass http://api_origin;
        }
    }
}

The key includes scheme, host, method, normalized URI, query string, country segment, and language. Add device class, experiment assignment, or another dimension whenever it changes the representation. Conversely, do not add arbitrary headers: unnecessary dimensions multiply objects and lower the hit rate.

Normalize query parameters deliberately

Using the raw $args is safe but can create a large number of equivalent objects when parameter order or tracking parameters vary. Build a normalized query value with map or application-level canonicalization, retaining only parameters that affect the response. Test that canonicalization never removes a functional parameter.

Choose country or region as the key

A region key has fewer objects and can improve reuse when all countries in the region truly share a representation. A country key provides stronger isolation. Omitting geography altogether risks serving one country’s content, price, inventory, or legal text to another country. Cache cardinality, hit rate, lock contention, and purge scope should be measured rather than guessed.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Protect misses and purges

For hot keys, configure cache locking so concurrent misses do not stampede the origin. Set bounded connect, send, and read timeouts. Purge or version cache objects when policy, localization, inventory, or legal content changes; waiting for TTL alone can leave stale data visible. NGINX Cookbook guidance covers cache keys, locking, bypass, purging, and performance as separate controls rather than one universal setting.

Put the pieces together and deploy safely

  1. Install or update the MMDB file and verify its permissions for the NGINX worker user.
  2. Load GeoIP2 and define the country variable before any map that uses it.
  3. Apply a native allow/deny map. Add a separate region map only for routing or cache segmentation that has been verified against product behavior.
  4. Add Lua only for decisions that require data or logic unavailable in static configuration. Keep refresh work asynchronous and bounded.
  5. Define cache bypass rules before defining the cache key. Test authenticated, session-cookie, non-GET, and error responses as explicitly non-shareable unless your application proves otherwise.
  6. Run nginx -t. If it succeeds, reload with nginx -s reload; a reload lets existing workers finish while new workers use the new configuration.
  7. Exercise requests from representative egress points, including an allowed country, a denied country, a VPN or proxy, and a request with authorization. Confirm status, upstream choice, cache status, and policy version in logs.

Observe correctness, speed, and cost

  • Lookup cost: local MMDB lookup is predictable; Lua adds execution and any policy-store dependency to the access path.
  • Cache cardinality: every added dimension creates more objects. Country isolation improves correctness but can reduce hit rate.
  • Origin protection: cache locking, bounded timeouts, and bypass rules prevent a cache miss from becoming an origin surge.
  • Freshness: monitor MMDB age, policy version propagation, and cache age separately.
  • Business impact: track denial rates, regional upstream distribution, cache-hit and bypass rates, origin errors, and purge completion. A sudden denial spike may indicate a stale database or malformed policy, not a traffic attack.
  • Geolocation limits: IP-based location can be wrong for VPNs, mobile networks, proxies, and corporate gateways. There is no universal accuracy or latency percentage to apply to every deployment.

Troubleshooting common failures

Every request is denied

Log the derived country variable and compare it with the MMDB’s expected result. Check that the module loaded, the database path is readable, and the map uses two-letter uppercase ISO codes. If a proxy hides client IPs, correct the trusted proxy chain before geolocation.

Users receive another country’s response

Inspect the cache key and response-varying headers. Add country or region, language, device, experiment, or authorization state when it changes the representation. Bypass caching for personalized responses and purge objects created with the old key.

Cache hit rate collapses

Look for high-cardinality query strings, unnecessary headers in the key, or a country key where a region key is genuinely safe. Normalize functional query parameters, remove tracking parameters, and compare hit rate against object count and correctness requirements.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Lua changes appear ignored

Production Lua code caching is expected. Run nginx -t, then nginx -s reload. Confirm the new worker generation is serving traffic and that the file loaded by require is the one you changed.

Origin load spikes during an outage

Check whether bypass rules, cache locking, or an origin-error policy is allowing every request through. Bound upstream timeouts, verify that only safe methods are cached, and decide explicitly how stale content should be handled during an origin failure.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If you need to verify how a regional, blocked, or consent-heavy page actually renders, ScreenshotNeo can capture it through one API request instead of maintaining a browser runner. Before capture it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each cleanup step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. Its MCP server exposes take_screenshot, get_page_info, and capture_pdf to Claude, Cursor, and other MCP clients.

Use the API documentation at https://screenshotneo.com/docs/ for authentication and options. A minimal request is:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/geo-test -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://example.com/geo-test"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com/geo-test' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

ScreenshotNeo includes full-page and element capture, device and retina settings, custom headers and cookies, waits, request blocking, signed links, asynchronous jobs, bulk capture, PDF output, and a usage API on every plan. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

FAQ

Do I need a city database for country blocking?

No. A country MMDB supplies the ISO country code needed for a country policy. Use city data only when region or city-level attributes are part of a tested requirement.

Should a cache key contain the HTTP method?

Include it when method semantics or the cache implementation could otherwise collide. In practice, configure caching for safe GET and HEAD requests and bypass other methods.

When is OpenResty preferable to native NGINX rules?

Choose it when decisions depend on signed or frequently changing policy, exceptions, or several signals. For a fixed country list, native maps have fewer moving parts.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Frequently Asked Questions

Can I use a 403 and a 451 response interchangeably?

No. Use 403 for a forbidden request. Reserve 451 for a restriction you intentionally identify as legal or regulatory.

What should I update first when a country policy changes?

Change the policy, then invalidate or version affected cache objects. MMDB freshness is a separate concern from policy propagation and cache invalidation.

Does NGINX Plus replace OpenResty Lua?

Not automatically. NGINX Plus adds documented packaging and API or key-value capabilities; OpenResty remains useful when programmable access-phase logic is the better fit.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.