NGINX / Server Setup

NGINX map Directive: Guide to Conditional Variables

by ,


We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

The NGINX map directive is one of the most powerful yet underutilized features in NGINX configuration. It allows you to create variables whose values depend on the values of other variables, enabling complex conditional logic without the performance penalty of multiple if statements. This guide covers everything you need to know about the NGINX map directive, from basic syntax to advanced real-world applications.

What is the NGINX map Directive?

The map directive creates a new variable whose value is determined by mapping the value of a source variable against a set of patterns. Think of it as a highly optimized switch statement or lookup table that runs at near-zero cost during request processing.

The basic syntax is:

map $source_variable $new_variable {
    default     default_value;
    pattern1    value1;
    pattern2    value2;
}

The map block must be placed in the http context, outside of any server block. This is because NGINX compiles the map into an efficient hash table during configuration loading, not during request processing.

Why Use the map Directive Instead of if Statements?

Many NGINX administrators reach for if statements when they need conditional logic. However, the if is evil principle in NGINX exists for good reason. The map directive offers several advantages:

Performance: Maps are compiled into hash tables at configuration load time. Lookups are O(1) for exact matches, making them extremely fast regardless of how many entries you have.

Cleaner Configuration: Instead of nested if blocks scattered throughout your configuration, maps centralize conditional logic in one place.

Reusability: A map-defined variable can be used anywhere in your configuration – in server blocks, location blocks, log formats, and more.

Safety: Unlike if statements, maps cannot cause unexpected behavior with other directives. For more on NGINX header configuration, see our guide on the pitfalls of add_header in NGINX.

Basic map Directive Examples

Detecting Mobile Devices

One of the most common uses for the map directive is device detection based on User-Agent:

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

The ~* prefix indicates a case-insensitive regular expression match. You can then use $is_mobile anywhere in your configuration:

server {
    listen 80;
    server_name example.com;

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

Setting Backend Servers Based on Hostname

You can use maps to route requests to different backend servers based on the incoming hostname:

map $host $backend_pool {
    default             "default_backend";
    example.com         "production";
    staging.example.com "staging";
    dev.example.com     "development";
}

upstream production {
    server 10.0.1.10:8080;
    server 10.0.1.11:8080;
}

upstream staging {
    server 10.0.2.10:8080;
}

upstream development {
    server 10.0.3.10:8080;
}

server {
    listen 80;
    server_name _;

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

Understanding map Pattern Matching

The NGINX map directive supports several pattern matching methods, evaluated in a specific order:

1. Exact String Matches

The simplest form matches the source variable exactly:

map $uri $cache_zone {
    default         "default_zone";
    /api/users      "users_cache";
    /api/products   "products_cache";
}

2. Regular Expression Patterns

Prefix a pattern with ~ for case-sensitive regex or ~* for case-insensitive:

map $uri $cache_duration {
    default                     "1h";
    "~^/api/v([0-9]+)/"        "5m";
    "~*[.](jpg|jpeg|png|gif)$"  "30d";
    "~*[.](css|js)$"            "7d";
}

Regular expressions can include capture groups. The captured values are available as $1, $2, etc., within the map value:

map $uri $api_version {
    default                 "v1";
    "~^/api/v([0-9]+)/"     "v$1";
}

For more details on NGINX patterns, check our article on NGINX location priority.

3. Wildcard Hostname Patterns

When the hostnames parameter is specified, NGINX enables special wildcard matching optimized for domain names:

map $host $site_tier {
    hostnames;
    default         "unknown";
    example.com     "main";
    *.example.com   "subdomain";
    .example.org    "wildcard";
    mail.*          "mail_server";
}

The hostnames parameter enables two wildcard types:

  • *.example.com – matches any subdomain (api.example.com, www.example.com)
  • .example.com – same as above, shorthand notation
  • mail.* – matches mail.example.com, mail.example.org, etc.

NGINX stores these wildcards in optimized hash tables, making lookups efficient even with thousands of entries.

Pattern Matching Priority

When multiple patterns could match, NGINX evaluates them in this order:

  1. Exact string match (highest priority)
  2. Prefix wildcard (*.example.com)
  3. Suffix wildcard (www.*)
  4. Regular expressions (in order of appearance)
  5. Default value (lowest priority)

The first match wins. For regular expressions, order matters – earlier patterns take precedence.

The default Parameter

Every map should include a default value. This value is used when no patterns match:

map $request_method $is_write_method {
    default     0;
    POST        1;
    PUT         1;
    PATCH       1;
    DELETE      1;
}

Security Warning: Omitting the default can lead to empty variable values, potentially bypassing security controls. Security tools like Gixy specifically check for maps without defaults and flag them as medium-severity issues.

The volatile Parameter

By default, NGINX caches map results per request. The volatile parameter disables this caching:

map $arg_timestamp $cache_buster {
    volatile;
    default     "";
    "~.+"       $arg_timestamp;
}

Use volatile when:
– The source variable changes during request processing
– You need fresh evaluation each time the variable is accessed
– The map result depends on variables that may be modified by other modules

In most cases, you should not use volatile as the default caching behavior improves performance.

The include Directive Within Maps

For large maps, you can split entries into separate files:

map $http_user_agent $is_bot {
    default     0;
    include     /etc/nginx/maps/known-bots.map;
}

The included file contains plain map entries:

# /etc/nginx/maps/known-bots.map
"~*googlebot"       1;
"~*bingbot"         1;
"~*yandexbot"       1;
"~*baiduspider"     1;
"~*duckduckbot"     1;

This approach is excellent for maintaining large blocklists or whitelists without cluttering your main configuration.

Tuning Map Performance

For configurations with many map entries, NGINX provides two tuning directives in the http context:

http {
    map_hash_max_size 2048;
    map_hash_bucket_size 128;
}

map_hash_max_size

Sets the maximum size of the hash table used for exact matches. Default is 2048. Increase this if you have many map entries and see warnings in the error log about hash table size.

map_hash_bucket_size

Sets the bucket size for the hash table. Default is typically 64 bytes (one cache line on most systems). Increase to 128 if your map keys are long strings.

These directives affect memory usage and configuration load time, not request processing time. Once loaded, lookups remain O(1). For more on NGINX performance tuning, see our guide on proxy_buffer_size optimization.

Real-World map Directive Use Cases

Rate Limiting with Maps

Create sophisticated rate limiting rules using maps:

# Determine rate limit key based on request type
map $request_uri $rate_limit_key {
    default             $binary_remote_addr;
    "~^/api/"           $binary_remote_addr$uri;
    "~^/login"          $binary_remote_addr;
    "~^/static/"        "";
}

# Whitelist certain IPs from rate limiting
map $remote_addr $is_whitelisted {
    default             0;
    127.0.0.1           1;
    "~^10[.]"           1;
    "~^192[.]168[.]"    1;
}

# Final rate limit key (empty = no limiting)
map $is_whitelisted $final_rate_key {
    default     $rate_limit_key;
    1           "";
}

limit_req_zone $final_rate_key zone=api_limit:10m rate=10r/s;

server {
    listen 80;

    location /api/ {
        limit_req zone=api_limit burst=20 nodelay;
        proxy_pass http://backend;
    }
}

This configuration:
– Applies different rate limiting to different URL patterns
– Exempts whitelisted IPs entirely
– Skips rate limiting for static files
– Uses a cascading map pattern for complex logic

URL Redirects with Maps

Handle legacy URL redirects efficiently:

map $uri $redirect_target {
    default                     "";
    /old-page                   /new-page;
    /legacy/products            /shop/products;
    "~^/blog/([0-9]+)/(.*)$"   /articles/$1/$2;
    /discontinued               https://archive.example.com/discontinued;
}

server {
    listen 80;
    server_name example.com;

    if ($redirect_target) {
        return 301 $redirect_target;
    }

    # Normal processing continues if no redirect
    location / {
        try_files $uri $uri/ /index.php?$args;
    }
}

This pattern handles hundreds of redirects with minimal configuration and optimal performance.

Dynamic CORS Configuration

Implement secure CORS using maps:

map $http_origin $cors_origin {
    default                             "";
    "~^https://(www[.])?example[.]com$" $http_origin;
    "~^https://.*[.]example[.]com$"     $http_origin;
}

map $cors_origin $cors_methods {
    default     "";
    "~.+"       "GET, POST, PUT, DELETE, OPTIONS";
}

map $cors_origin $cors_headers {
    default     "";
    "~.+"       "Content-Type, Authorization, X-Requested-With";
}

map $cors_origin $cors_credentials {
    default     "";
    "~.+"       "true";
}

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

    add_header Access-Control-Allow-Origin $cors_origin always;
    add_header Access-Control-Allow-Methods $cors_methods always;
    add_header Access-Control-Allow-Headers $cors_headers always;
    add_header Access-Control-Allow-Credentials $cors_credentials always;

    location / {
        if ($request_method = OPTIONS) {
            return 204;
        }
        proxy_pass http://backend;
    }
}

This approach only sends CORS headers for allowed origins, preventing information disclosure to unauthorized origins.

A/B Testing Implementation

Implement server-side A/B testing:

# Use split_clients for random assignment
split_clients "${remote_addr}${request_uri}" $ab_random {
    50%     "A";
    *       "B";
}

# Override with cookie if user already assigned
map $cookie_ab_variant $ab_variant {
    default     $ab_random;
    "A"         "A";
    "B"         "B";
}

# Select backend based on variant
map $ab_variant $ab_backend {
    default     "control_backend";
    "A"         "control_backend";
    "B"         "experiment_backend";
}

server {
    listen 80;
    server_name example.com;

    location / {
        add_header Set-Cookie "ab_variant=$ab_variant; Path=/; Max-Age=86400";
        proxy_pass http://$ab_backend;
        proxy_set_header X-AB-Variant $ab_variant;
    }
}

Content Security Policy Per Route

Apply different security policies to different parts of your application:

map $uri $csp_policy {
    default         "default-src 'self'; script-src 'self'";
    "~^/admin/"     "default-src 'self'; script-src 'self' 'unsafe-inline'";
    "~^/embed/"     "frame-ancestors *";
    "~^/api/"       "default-src 'none'";
}

server {
    listen 443 ssl;
    server_name example.com;

    add_header Content-Security-Policy $csp_policy always;

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

Cascading Maps Pattern

You can chain multiple maps together for complex conditional logic:

# First map: determine request category
map $uri $request_category {
    default         "page";
    "~^/api/"       "api";
    "~^/static/"    "static";
    "~^/admin/"     "admin";
}

# Second map: determine cache duration based on category
map $request_category $cache_duration {
    default     "1h";
    api         "0";
    static      "30d";
    admin       "0";
}

# Third map: determine log format based on category
map $request_category $log_format_name {
    default     "main";
    api         "api_detailed";
    static      "minimal";
    admin       "admin_audit";
}

This cascading pattern keeps each map focused on a single decision, improving maintainability.

Common Mistakes and How to Avoid Them

Mistake 1: Placing map in Server Context

Maps must be in the http context:

# WRONG - will cause configuration error
server {
    map $host $backend {
        example.com backend1;
    }
}

# CORRECT
http {
    map $host $backend {
        default     backend1;
        example.com backend2;
    }

    server {
        # Use $backend here
    }
}

Mistake 2: Forgetting the Default

Always include a default to prevent empty variables:

# RISKY - no default
map $http_x_custom_header $allowed {
    "secret-token"  1;
}

# SAFE - explicit default
map $http_x_custom_header $allowed {
    default         0;
    "secret-token"  1;
}

Mistake 3: Incorrect Regex Escaping

Remember that map values are not automatically quoted:

# WRONG - dot matches any character
map $host $backend {
    api.example.com     "api_backend";
}

# CORRECT for literal matching (no regex needed for exact match)
map $host $backend {
    default             "default_backend";
    api.example.com     "api_backend";
}

# CORRECT for regex with literal dot
map $host $backend {
    default                 "default_backend";
    "~^api[.]example[.]com$" "api_backend";
}

Mistake 4: Using Regular Expressions When Not Needed

Exact matches are faster than regex:

# SLOWER - unnecessary regex
map $request_method $is_post {
    default     0;
    "~^POST$"   1;
}

# FASTER - exact match
map $request_method $is_post {
    default     0;
    POST        1;
}

Debugging map Variables

To debug map variable values, add them to your log format or response headers:

log_format debug_maps '$remote_addr - $request_uri - '
                      'mobile=$is_mobile backend=$backend_pool';

server {
    access_log /var/log/nginx/debug.log debug_maps;

    location / {
        add_header X-Debug-Mobile $is_mobile;
        add_header X-Debug-Backend $backend_pool;
        # ...
    }
}

You can also use curl to test your maps:

# Test mobile detection
curl -H "User-Agent: Mozilla/5.0 (iPhone)" http://localhost/ -I

# Test hostname routing
curl -H "Host: staging.example.com" http://localhost/ -I

Security Considerations

Validate map Configurations with Gixy

Gixy is an NGINX configuration analyzer that checks for common security issues, including maps without defaults. Run it as part of your deployment pipeline:

gixy /etc/nginx/nginx.conf

Gixy will warn you about:
– Maps without default values
– Potentially dangerous regular expressions
– Variables that could contain dangerous characters

Avoid User Input in Sensitive Contexts

Be careful when map results are used in security-sensitive contexts:

# POTENTIALLY DANGEROUS if $uri contains user input
map $uri $allowed_origin {
    "/api/public"   "*";
    default         "";
}

add_header Access-Control-Allow-Origin $allowed_origin;

Always validate that your map patterns cannot be exploited through crafted input.

Performance Benchmarks

To understand map performance, consider that:

  • Exact match lookups: O(1) hash table lookup, typically under 100 nanoseconds
  • Wildcard lookups: O(1) with optimized hash tables for DNS patterns
  • Regex lookups: O(n) where n is the number of regex patterns, evaluated sequentially

For a map with 1000 exact entries, lookup time is essentially the same as a map with 10 entries. However, 1000 regex patterns will be significantly slower than 10.

Conclusion

The NGINX map directive is an essential tool for any serious NGINX administrator. It provides a clean, performant, and maintainable way to implement conditional logic in your NGINX configuration. By mastering maps, you can:

  • Eliminate messy if statement chains
  • Implement sophisticated routing and rate limiting
  • Create dynamic security policies
  • Build A/B testing infrastructure
  • Manage URL redirects efficiently

Remember to always include default values, use exact matches when possible, and validate your configurations with tools like Gixy. With these practices in place, the map directive becomes one of the most valuable tools in your NGINX toolkit.

D

Danila Vershinin

Founder & Lead Engineer

NGINX configuration and optimizationLinux system administrationWeb performance engineering

10+ years NGINX experience • Maintainer of GetPageSpeed RPM repository • Contributor to open-source NGINX modules

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

This site uses Akismet to reduce spam. Learn how your comment data is processed.