If you’ve spent any real time editing an nginx.conf file, you’ve probably noticed those little tokens scattered around that start with a dollar sign — things like $host, $remote_addr, or $request_uri. Those are Nginx variables, and once you understand how they work, you’ll start writing configurations that are dramatically more flexible than the static, hardcoded blocks most tutorials show you.
I want to walk you through what Nginx variables actually are, how they’re evaluated, how to create your own, and where they trip people up. By the end of this guide you’ll be comfortable using variables for logging, routing, access control, and rewriting — and you’ll understand why Nginx variables don’t behave quite like variables in a normal programming language.
What Are Nginx Variables, Really?
Nginx variables are named placeholders that get evaluated at request time. Unlike a variable in Python or Bash, an Nginx variable isn’t “set once and read later” in the traditional sense — it’s more like a lazy-evaluated expression tied to the lifecycle of a single HTTP request. Every time a request comes in, Nginx computes the value of a variable the first time it’s referenced, caches that value for the rest of the request, and then throws it away once the request finishes.
This matters because it explains a lot of confusing behavior beginners run into. For example, if you try to use if blocks combined with variables in the wrong context, or if you expect a variable to persist across requests like a global counter, you’ll be disappointed. Nginx variables live and die with the request.
There are two broad categories:
- Built-in variables — provided by Nginx core and its modules (like
ngx_http_core_module,ngx_http_rewrite_module,ngx_http_ssl_module, etc.). Examples include$uri,$args,$http_user_agent,$scheme,$ssl_protocol. - User-defined variables — variables you create yourself using the
setdirective or as a side effect of directives likerewrite,map, orgeo.
Why Bother With Variables At All?
You might be wondering why you’d need variables when you could just hardcode values into your location blocks. The honest answer is: for simple sites, you often don’t need them. But variables become essential the moment you want your configuration to adapt based on the incoming request — things like:
- Routing requests differently based on the
Hostheader (virtual hosting logic) - Building custom log formats that capture extra request metadata
- Implementing conditional logic without duplicating entire server blocks
- Passing dynamic values to upstream servers via proxy headers
- Doing A/B testing or canary routing based on cookies or headers
- Rewriting URLs based on patterns extracted from the request
Once your config needs to “think” a little, variables are how you give it that ability.
Requirements
Before you start experimenting, make sure you have:
- Nginx installed (open source or Nginx Plus — this guide covers open source behavior, which is what most people run). You can check your version with:
nginx -v
- Root or sudo access to edit configuration files, typically under
/etc/nginx/. - A basic working
serverblock already in place, since we’ll be modifying it rather than starting from zero. - Comfort with reloading Nginx safely:
sudo nginx -t && sudo systemctl reload nginx
I always run nginx -t before reloading. It validates syntax without taking the server down, and it has saved me from outages more times than I’d like to admit.
Built-in Variables You’ll Use Constantly
Here are the variables I reach for most often in real configurations:
$host— the value of the Host header, or the server name matching the request if the header is absent.$remote_addr— the client’s IP address.$request_uri— the full original request URI, including the query string, exactly as the client sent it.$uri— the current URI in the request, after internal rewrites (this can change during processing, unlike$request_uri).$args— the query string arguments.$scheme—httporhttps.$http_HEADERNAME— any incoming request header, referenced by lowercasing the header name and replacing dashes with underscores. So theUser-Agentheader becomes$http_user_agent.$sent_http_HEADERNAME— similarly, for response headers you’re sending.$status— the HTTP response status code (available in log contexts).$request_time— how long the request took to process, useful for performance logging.$ssl_protocoland$ssl_cipher— details about the TLS handshake, handy for security auditing.
A subtle but important distinction: $uri vs $request_uri. $request_uri never changes during request processing — it’s the raw, original URI. $uri can change as Nginx performs internal rewrites (for example, via rewrite or try_files). If you’re debugging weird rewrite behavior, logging both side by side will save you a lot of head-scratching.
Creating Your Own Variables with set
The set directive, part of the rewrite module, lets you define a custom variable:
server {
listen 80;
server_name example.com;
set $my_custom_var "hello";
location / {
add_header X-Custom-Header $my_custom_var;
return 200 "Value is: $my_custom_var\n";
}
}
A few rules to keep in mind:
- Custom variable names must start with a dollar sign when referenced, but you declare them with
set $name value;. - You can’t declare the same variable twice with conflicting types in incompatible contexts — Nginx will complain if you have a naming collision with an existing variable.
setcan appear inserver,location, andifcontexts.
Combining Variables and Conditionals
This is where variables start to earn their keep. A common pattern is detecting something about the request and adjusting behavior:
server {
listen 80;
server_name example.com;
set $mobile_redirect "no";
if ($http_user_agent ~* "(android|iphone|mobile)") {
set $mobile_redirect "yes";
}
location / {
if ($mobile_redirect = "yes") {
return 302 https://m.example.com$request_uri;
}
root /var/www/example.com;
index index.html;
}
}
I want to be upfront about something: Nginx’s documentation itself warns that if is “evil” in certain contexts, particularly when used inside location blocks for anything beyond return or rewrite. It doesn’t behave like a conditional in a general-purpose language — it can produce surprising results when combined with other directives in the same block. For anything beyond a simple check, the map directive (covered next) is the safer, more idiomatic tool.
The map Directive: The Right Way to Do Conditional Variables
map creates a new variable whose value depends on another variable, using a lookup table. It’s evaluated once per request and doesn’t suffer from the same pitfalls as if.
http {
map $http_user_agent $is_bot {
default 0;
"~*bot|crawler|spider" 1;
}
server {
listen 80;
server_name example.com;
location / {
if ($is_bot) {
return 403;
}
root /var/www/example.com;
}
}
}
Note that map must live in the http context, not inside server or location. This trips up a lot of people who try to nest it directly inside a virtual host block.
Another practical example — routing based on a custom header for canary deployments:
http {
map $http_x_canary $backend_pool {
default "stable_backend";
"true" "canary_backend";
}
upstream stable_backend {
server 10.0.0.10:8080;
}
upstream canary_backend {
server 10.0.0.20:8080;
}
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://$backend_pool;
}
}
}
This lets you send a specific subset of traffic (anyone sending X-Canary: true) to a different backend pool entirely, without duplicating your whole server block.
Using Variables in Logging
Custom log formats are one of the most practical uses of variables. Here’s a log format that captures more than the default combined format:
http {
log_format detailed '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
server {
access_log /var/log/nginx/access.log detailed;
...
}
}
The upstream timing variables ($upstream_response_time, $upstream_connect_time, $upstream_header_time) are gold when you’re trying to figure out whether slowness is coming from Nginx itself or from your backend application.
Complete Example Configuration
Here’s a fuller example tying several of these ideas together — a config that logs detailed timing, redirects mobile users, and routes canary traffic:
http {
log_format detailed '$remote_addr - [$time_local] "$request" '
'$status $body_bytes_sent rt=$request_time '
'ua="$http_user_agent"';
map $http_user_agent $is_mobile {
default 0;
"~*android|iphone|ipad|mobile" 1;
}
map $http_x_canary $backend_pool {
default "stable_backend";
"true" "canary_backend";
}
upstream stable_backend {
server 10.0.0.10:8080;
}
upstream canary_backend {
server 10.0.0.20:8080;
}
server {
listen 80;
server_name example.com;
access_log /var/log/nginx/example.access.log detailed;
location / {
if ($is_mobile) {
return 302 https://m.example.com$request_uri;
}
proxy_pass http://$backend_pool;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
Testing Your Configuration
Always validate syntax before reloading:
sudo nginx -t
To actually verify variable behavior, I like using curl with custom headers to simulate different clients:
# Simulate a mobile user agent
curl -A "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)" -I http://example.com/
# Simulate canary header
curl -H "X-Canary: true" -I http://app.example.com/
You can also temporarily add a debug endpoint that echoes variable values back, which is invaluable while you’re still building out logic:
location /debug {
default_type text/plain;
return 200 "host=$host uri=$uri args=$args is_mobile=$is_mobile\n";
}
Just remember to remove or restrict access to that endpoint before going to production — you don’t want to leak internal routing logic or headers to the public internet.
Troubleshooting Common Issues
“unknown directive” or “invalid variable name” errors on reload. This almost always means you’re referencing a variable that doesn’t exist yet, or you have a typo. Nginx variable names are case-sensitive and must be all lowercase with underscores by convention.
A variable seems “stuck” at its default value. Check whether you’re evaluating it before it’s set. Directive order inside a block doesn’t matter for set, but map blocks must be declared in http context before the server block uses them, or Nginx will fail to start.
if behaves unpredictably when mixed with other directives. As mentioned earlier, avoid combining if with things like proxy_pass selection logic in complex ways. If you find yourself nesting multiple if blocks, refactor into map.
Custom variables not appearing in logs. Double check your log_format directive is actually referenced by the access_log directive in the relevant server block — defining a format doesn’t automatically apply it.
Security Considerations
Variables derived directly from client input — like $http_user_agent, $http_referer, or any custom header — should never be trusted blindly. If you’re using them in proxy_pass targets, return statements, or anywhere that could be interpreted as a path or command, sanitize or validate them first. A classic mistake is doing something like proxy_pass $http_x_backend; without validating that header, which opens the door to request smuggling or SSRF-style abuse if an attacker controls that header.
Similarly, be cautious with logging raw user input into log files. Header injection via newline characters has historically been an issue in poorly configured web servers, so keep Nginx updated and avoid manually concatenating unsanitized variables into shell commands if you’re scripting around your logs.
Performance Tips
Variables themselves are cheap — Nginx’s variable system is designed to be fast, with lazy evaluation meaning a variable’s value is only computed if something actually references it during that request. That said:
- Avoid excessive
ifblocks in hot paths;mapis generally faster and clearer since it’s a single lookup rather than sequential conditional evaluation. - Keep regular expressions in
mapblocks reasonably simple. Complex regex evaluated on every request can add measurable latency under high traffic. - Don’t log variables you don’t need. Every additional field in a custom log format adds a small amount of overhead per request, and it adds up at scale.
Real-World Use Cases
- Blue-green and canary deployments: routing a percentage of traffic or specific users to a new backend version using headers or cookies mapped to upstream names.
- Geo-based content: combining the
geomodule withmapto serve region-specific content or apply different rate limits per country. - Multi-tenant SaaS routing: extracting a subdomain into a variable and using it to route to tenant-specific backends or root directories.
- Detailed observability: enriching access logs with upstream timing variables to build dashboards that separate Nginx overhead from application latency.
- Feature flagging at the edge: using cookies mapped to variables to toggle between old and new frontend builds without touching application code.
Best Practices
- Prefer
mapoveriffor anything beyond the simplest conditional. - Keep variable names descriptive —
$backend_poolis far more maintainable than$x1. - Centralize your
mapblocks in a dedicated included file (likeconf.d/maps.conf) if you have many of them, so your server blocks stay readable. - Always test with
nginx -tbefore reloading, and keep a rollback plan (a copy of the last known-good config) handy. - Treat any variable sourced from client input as untrusted data.
- Document non-obvious variables with a comment explaining their purpose — future you (or a teammate) will thank you.
Once variables click, you start seeing Nginx less as a static file server and more as a lightweight request-processing engine. That shift in mindset is genuinely useful, whether you’re just serving a personal blog or running edge routing logic for a fleet of microservices.