Geo / map / variable cheatsheet.

map

map $http_user_agent $is_mobile {
    default 0;
    ~*android 1;
    ~*iphone  1;
    ~*ipad    1;
}

server {
    location / {
        if ($is_mobile) {
            return 301 https://m.example.com$request_uri;
        }
    }
}

map with multiple values

map $http_accept_language $lang {
    default "en";
    ~^en "en";
    ~^es "es";
    ~^de "de";
}

map for cache key

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

geo

geo $internal {
    default 0;
    10.0.0.0/8 1;
    192.168.0.0/16 1;
    127.0.0.1 1;
}

server {
    location /admin {
        if ($internal = 0) { return 403; }
    }
}

By IP, not header.

GeoIP2 (country lookup)

apt install libnginx-mod-http-geoip2
http {
    geoip2 /etc/nginx/GeoLite2-Country.mmdb {
        auto_reload 5m;
        $geoip2_country_code default=XX country iso_code;
        $geoip2_country_name country names en;
    }
    
    map $geoip2_country_code $allowed {
        default 1;
        CN 0;
        RU 0;
    }
    
    server {
        if ($allowed = 0) { return 451; }
    }
}

split_clients (A/B)

split_clients $request_id $variant {
    50% A;
    50% B;
}

map $variant $upstream {
    A app_a;
    B app_b;
}

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

Custom variables via map

map $request_method:$content_type $cache_action {
    default "skip";
    "GET:" "cache";
    "GET:application/json" "cache";
}

include map files

http {
    map $http_user_agent $bot {
        include /etc/nginx/maps/bots.conf;
    }
}

/etc/nginx/maps/bots.conf:

default 0;
~*Googlebot 1;
~*Bingbot 1;
~*DuckDuckBot 1;

Boolean operators (workaround)

if doesn’t support &&, ||. Workaround:

set $combined "$is_mobile-$is_authenticated";

if ($combined = "1-0") {
    # mobile + anonymous
    return 301 /mobile-login;
}

Or use map:

map "$is_mobile:$is_authenticated" $action {
    "1:0" "mobile-login";
    "1:1" "mobile-home";
    default "desktop";
}

Variables in proxy_pass

location / {
    proxy_pass http://$upstream;       # $upstream from map
}

⚠️ With variable, nginx skips upstream block caching of resolution; uses runtime resolver:

resolver 1.1.1.1 valid=300s;

limit_req_zone with map

map $http_x_skip_limit $no_limit {
    default 0;
    "secret-token" 1;
}

map $no_limit $rate_key {
    0 $binary_remote_addr;
    1 "";
}

limit_req_zone $rate_key zone=api:10m rate=10r/s;

$upstream_addr / $upstream_status

Log which backend served:

log_format main '... upstream=$upstream_addr status=$upstream_status rt=$request_time';

Useful for debugging LB.

$request_id

add_header X-Request-ID $request_id;
proxy_set_header X-Request-ID $request_id;

UUID per request. Correlate logs across nginx + app.

Common variables

$args              # query string
$arg_NAME          # specific query arg
$cookie_NAME       # cookie
$http_NAME         # request header
$sent_http_NAME    # response header
$upstream_http_NAME # upstream response header
$host              # Host header
$server_name
$server_port
$scheme
$remote_addr
$remote_port
$remote_user       # basic auth user
$request           # full request line
$request_method
$request_uri       # full URI (path + query)
$uri               # decoded URI (no query)
$document_root
$document_uri
$body_bytes_sent
$bytes_sent
$status
$request_time
$request_length
$pid
$hostname
$msec              # high-res time
$time_iso8601
$time_local

Common mistakes

  • if inside location — nginx wiki says “If is evil.” Use map or return when possible.
  • Map names overlap with vars — confusing.
  • GeoIP2 mmdb path wrong / not auto-reloaded.
  • Forgetting resolver for variable proxy_pass.
  • $args vs $query_string (synonyms).

Read this next

If you want my A/B testing + GeoIP recipes, they’re at rajpoot.dev .


Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .