Built-in Variables #
The Nginx web server doesn’t process requests in a static, rigid way. Under the hood, Nginx provides a very powerful dynamic variable evaluation engine. Variables in Nginx aren’t like variables in traditional imperative programming languages, whose values are statically allocated in memory from the start. In Nginx, variables are allocated per request inside the memory pool, and their values are evaluated dynamically (on-the-fly) only when a module or directive actually references them.
Understanding Nginx’s built-in (embedded) variables will make your configuration files far smarter, more dynamic, and more adaptive to user behavior. This article presents a complete classification of Nginx’s built-in variables, dissects the difference between confusing variables (like $host vs $http_host), explains the safe implementation of the map module based on lazy evaluation, and debunks the myths and facts behind the famous “If is Evil” warning in the Nginx world.
Complete Categories of Nginx Built-in Variables #
Nginx provides hundreds of built-in variables automatically populated from request data, TCP connection status, TLS encryption, and upstream backend performance. Here’s a complete reference to the variables most frequently used in production environments:
1. Request Variables (HTTP Headers & Payload) #
These variables extract information directly from the HTTP request packet sent by the client browser:
$uri- The current URL path after normalization (decoded from URL-encoding, query string removed, and special folders like
../processed into a clean path). This value can change mid-request if you do an internal redirect (for example usingtry_filesorrewrite).
- The current URL path after normalization (decoded from URL-encoding, query string removed, and special folders like
$request_uri- The original raw request URI exactly as sent by the client browser, including the query string. This value is constant (read-only) and never changes throughout the request’s lifecycle.
- Example: If a client visits
/search?q=nginx, then$uriis/search, while$request_uriis/search?q=nginx.
$args- The entire query string after the question mark (e.g.,
q=nginx&page=2).
- The entire query string after the question mark (e.g.,
$arg_NAME- Retrieves the value of a specific query string parameter by its name.
- Example: The
$arg_qvariable returns the valuenginxfrom the query string?q=nginx.
$request_method- The HTTP method used (e.g.,
GET,POST,PUT,DELETE).
- The HTTP method used (e.g.,
$http_NAME- Retrieves the value of any HTTP request header by converting the header name to lowercase and replacing hyphens (
-) with underscores (_). - Example:
$http_user_agent(User-Agent header),$http_cookie(Cookie header),$http_x_forwarded_for(origin IP behind a proxy).
- Retrieves the value of any HTTP request header by converting the header name to lowercase and replacing hyphens (
2. Connection and Network Server Variables #
Retrieves TCP socket parameters and physical network connection status:
$remote_addr- The IP address of the client directly connected to Nginx. If your server sits behind a Load Balancer (like Cloudflare or AWS ALB), this variable contains the Load Balancer’s IP, not the visitor’s real IP.
$binary_remote_addr- The client IP address in binary format (4 bytes for IPv4, 16 bytes for IPv6). This variable is very memory-efficient and is the mandatory key for rate limiting (
limit_req_zone).
- The client IP address in binary format (4 bytes for IPv4, 16 bytes for IPv6). This variable is very memory-efficient and is the mandatory key for rate limiting (
$scheme- The protocol used by the client, returning the value
"http"or"https".
- The protocol used by the client, returning the value
$server_port- The server port receiving the request (usually
80or443).
- The server port receiving the request (usually
3. SSL/TLS Variables (Encryption) #
Only available if the client connection uses the HTTPS protocol:
$ssl_protocol- The agreed TLS protocol version (e.g.,
TLSv1.2,TLSv1.3).
- The agreed TLS protocol version (e.g.,
$ssl_cipher- The encryption cipher algorithm used (e.g.,
ECDHE-RSA-AES128-GCM-SHA256).
- The encryption cipher algorithm used (e.g.,
4. Upstream Variables (Backend) #
Available after Nginx forwards the request to a backend server using proxy_pass or fastcgi_pass:
$upstream_addr- The IP address and port of the backend server that processed the request (e.g.,
127.0.0.1:8080).
- The IP address and port of the backend server that processed the request (e.g.,
$upstream_status- The HTTP status code returned by the backend (e.g.,
200,500).
- The HTTP status code returned by the backend (e.g.,
$upstream_response_time- The time the backend spent processing the request in seconds (millisecond precision).
$upstream_cache_status- Nginx’s caching status for that request (
HIT,MISS,BYPASS,EXPIRED).
- Nginx’s caching status for that request (
5. Time and Duration Variables #
$time_iso8601- The local time when the request was received in ISO 8601 format (e.g.,
2026-01-15T10:30:45+07:00). Highly recommended for modern log formats so log parsers (like the ELK Stack) can read them easily.
- The local time when the request was received in ISO 8601 format (e.g.,
$request_time- The total time Nginx spent serving the request (in seconds with millisecond precision), measured from the first byte received from the client until the last byte is sent and the connection closes.
Case Study: $host vs $http_host #
One of the biggest confusions when configuring a reverse proxy is choosing between the $host and $http_host variables to forward to the backend via the proxy_set_header Host directive. This small difference has a big functional and security impact:
1. The $http_host Variable (Raw & Rigid)
#
This variable takes the raw value of the Host header sent by the client browser without any modification.
- Behavior: If the browser sends a request to a non-standard port (e.g.,
example.com:8080), the$http_hostvariable isexample.com:8080. If theHostheader is empty or not sent by the client, this variable is empty.
2. The $host Variable (Normalized & Safe)
#
This variable is designed to be more robust and secure. Nginx computes the $host value with the following fallback order:
- If there’s a
Hostheader from the client, Nginx uses it after stripping the port information (e.g.,example.com:8080is normalized toexample.com) and converting it to lowercase. - If the
Hostheader is absent or empty, Nginx uses the matching server block name (server_namedirective). - If no server block matches, Nginx uses the server IP address that received the connection.
Input Case Comparison Table: #
| Incoming Request Characteristic | $http_host Value | $host Value (Recommended) |
|---|---|---|
Request to example.com | example.com | example.com |
Request to example.com:8080 | example.com:8080 (Contains port) | example.com (Port stripped) |
Empty / Missing Host Header | (Empty) | our_server_block_name (Safe fallback) |
| Host Header Injection Attack | hacker_domain.com | our_server_block_name (Safe fallback if filtered) |
- Best Practice: Always use
$hostinproxy_set_headerfor reverse proxies. This prevents your backend from getting confused by non-standard port information and protects applications from Host Header Injection attacks.proxy_set_header Host $host;
The Lifecycle of Time Variables: Request Time vs Upstream Time #
To make debugging performance bottlenecks in production easier, you need to understand how Nginx calculates request durations using the $request_time and $upstream_response_time variables.
Here’s a sequence diagram visualizing the request processing lifecycle and where those time variables are measured:
sequenceDiagram
autonumber
actor Klien
participant Nginx
participant Backend as Upstream (Go/Node)
Klien->>Nginx: TCP Handshake & Start Sending First Request Bytes
Note over Nginx: Start of $request_time measurement
Klien->>Nginx: Finished sending Request Headers & Body
Nginx->>Backend: Open TCP & Forward Request (proxy_pass)
Note over Nginx: Start of $upstream_response_time measurement
Backend->>Nginx: Send first Response Byte
Backend->>Nginx: Finished sending all Response Data
Note over Nginx: End of $upstream_response_time
Nginx->>Klien: Send Response Data to client (over internet bandwidth)
Klien->>Nginx: Confirm receipt of last byte & Close TCP Connection
Note over Nginx: End of $request_time (Logged)From the diagram above, we can conclude:
- If the client’s internet connection is very slow (e.g., on mobile networks), the
$request_timevalue will be much larger than$upstream_response_time(because steps 1 to 2 and steps 7 to 8 take a long time). - If your backend is slow at processing database queries, the
$upstream_response_timevalue will swell, which automatically pulls up the$request_timevalue. - Comparing these two variables in access logs is very useful for identifying whether web slowness is caused by the client network or a server-side database bottleneck.
Safe Conditional Logic with the map Module (Lazy Evaluation) #
Inside Nginx, writing conditional branching logic (like: “if the request is POST, do X; if it’s a mobile browser, redirect to Y”) using if blocks is high-risk (see the If is Evil discussion below). The best and most efficient solution Nginx provides is the map module.
map Syntax and How It Works #
The map block can only be written in the http context (global), outside server blocks. map takes one input variable, compares it against matching patterns, and returns a new value into an output variable.
http {
# Syntax: map input_variable output_variable { ... }
map $request_method $cache_bypass {
default 0;
POST 1;
PUT 1;
DELETE 1;
}
server {
location / {
proxy_pass http://backend;
proxy_cache my_cache;
# Use the mapped result variable
proxy_cache_bypass $cache_bypass;
}
}
}
The Remarkable Advantages of the map Module: #
- Lazy Evaluation: Nginx doesn’t process the rules inside a
mapblock when a request first arrives. Evaluation only happens when the output variable (in the example:$cache_bypass) is referenced for the first time in that request. If the variable is never referenced, the CPU doesn’t waste cycles processing that mapping. - Hash Table Optimization: The matching patterns inside
mapare compiled into a static hash table in memory at startup. Domain or string matching runs in constant $\mathcal{O}(1)$ time, far faster than evaluating a chain ofifblocks sequentially.
Here’s a visual diagram illustrating the Lazy Evaluation mechanism of Nginx’s map module:
flowchart TD
Init["Request Enters Nginx"] --> Phase1["Initialization Phase: <br> Nginx does NOT process the 'map' block"]
Phase1 --> Phase2["Content Phase: <br> Nginx executes the 'proxy_cache_bypass $cache_bypass' directive"]
Phase2 --> CallVar{"Does the $cache_bypass variable <br> already have a value?"}
CallVar -->|"Yes (Already evaluated)"| UseVal["Use the existing value in RAM"]
CallVar -->|"No (First evaluation)"| EvalMap["1. Read the current input value ($request_method) <br> 2. Look up the match in the map hash table <br> 3. Write the result to the output variable $cache_bypass"]
EvalMap --> SaveVal["Save the value to the request memory cache"]
SaveVal --> UseVal
UseVal --> Respond["Execute the cache bypass action"]
style Phase2 stroke:#f57c00,stroke-width:2px
style EvalMap stroke:#388e3c,stroke-width:2pxCustom Logging and Distributed Tracing with Variables #
Nginx variables are crucial for building modern application monitoring strategies. In production environments, standard Nginx logs are often not enough to trace a request’s journey through dozens of microservice containers.
1. Tracking Transactions with $request_id
#
Nginx provides the $request_id variable containing a unique 32-character hex string randomly generated for each incoming request. This variable acts as a global Correlation ID.
You can flow this $request_id to your backend applications and return it to the client browser to make error log tracking easier:
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://localhost:8080;
# Send the Correlation ID to the backend server
proxy_set_header X-Request-ID $request_id;
# Return the Correlation ID to the client browser (for bug reports)
add_header X-Request-ID $request_id;
}
}
If a user sees an error in their browser, they just report that X-Request-ID header to the IT team. The IT team can copy that ID and find the exact same log records in Elasticsearch, the backend database, or Nginx logs instantly.
2. Complete Production Access Log Format #
Here’s an example of creating a custom log format containing in-depth backend I/O performance analysis:
http {
# Define a custom log format named 'production_trace'
log_format production_trace '$remote_addr - $remote_user [$time_iso8601] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'request_id="$request_id" '
'req_time=$request_time '
'up_conn_time=$upstream_connect_time '
'up_header_time=$upstream_header_time '
'up_resp_time=$upstream_response_time '
'cache_status=$upstream_cache_status';
# Enable the access log using the format above
access_log /var/log/nginx/access.log production_trace;
}
$upstream_connect_time: The time (seconds) Nginx took to do the TCP/TLS handshake with the backend server. If this value swells, there’s a backend overload bottleneck or a local network problem.$upstream_header_time: The time spent waiting for the backend to send the first HTTP response header. This reflects your backend application’s compute performance (e.g., slow database queries).
Mythbuster: Why Is the “if” Directive in Location Blocks Dangerous? #
In the Nginx community, there’s a stern warning: “If is Evil”. System administrators are strongly discouraged from using the if directive inside location blocks for most scenarios, because its behavior is often unintuitive and can cause server crashes or security holes.
Why Is if Dangerous? (Technical Explanation)
#
Nginx doesn’t process configuration files procedurally like an ordinary programming language. Nginx executes configuration through 11 rigid request processing phases.
The if directive is part of the Rewrite module. Therefore, it’s evaluated very early in the rewrite phase, even before Nginx determines which content module (like the static file handler or proxy_pass) should serve the request.
When an if condition is true, Nginx internally performs an extreme action:
- Nginx creates a hidden anonymous location block in RAM.
- The request is forcibly redirected into that anonymous location.
- This anonymous location doesn’t inherit most of the important directives from the outer location (like proxy header configs, security filters, or zone limits).
Example of a Fatal if Bug (Anti-Pattern):
#
# ANTI-PATTERN: Causes 404 errors or stuck requests!
location / {
proxy_pass http://backend;
# We want to add a special header for requests from a specific IP
if ($remote_addr = 192.168.1.100) {
add_header X-Special-Client "true";
}
}
- Result: When a client with IP
192.168.1.100visits the website, Nginx evaluates theif, the condition is true, and the request enters the anonymous location. Because this anonymous location doesn’t inheritproxy_pass http://backend;from outside, Nginx gets confused about how to process the request. As a result, Nginx tries to serve a static file from the default root folder, fails to find the file, and returns a404 Not Founderror to the client, even though the backend is actively running.
100% Safe if Usage Scenarios
#
The only if usage guaranteed 100% safe by the Nginx core team is when, inside that if block, you only use the following two directives:
return ...(Returning an instant response, likereturn 301orreturn 403).rewrite ... last;(Stopping the rewrite phase and restarting the location search).
Both directives are safe because they immediately cut off the request processing flow and return control to the Nginx event loop instantly, so the hidden anonymous location never gets a chance to cause inheritance problems.
# SAFE USAGE: Only contains return at the server context level
server {
listen 80;
server_name example.com;
# Safe because it exits immediately via return 301
if ($host = 'www.example.com') {
return 301 https://example.com$request_uri;
}
}
Summary #
- Nginx variables are evaluated dynamically (on-the-fly) per request, not statically allocated at system startup.
- Always use
$hostinstead of$http_hostwhen forwarding the Host header to upstream backends to avoid Host Header Injection attacks and clean up non-standard port information.$request_timemeasures the full request cycle duration from the client’s perspective (sensitive to slow bandwidth), while$upstream_response_timepurely measures your backend’s compute performance.- Distributed Tracing can be enabled instantly using the
$request_idvariable as a global Correlation ID between microservices.- The
mapmodule leverages lazy evaluation (only processing if referenced) and fast in-memory hash tables, making it a highly efficient and safe replacement forifblocks.- Avoid
ifinside location blocks unless the block only containsreturnorrewrite ... lastinstructions, to avoid fatal bugs caused by hidden anonymous location creation.