Note: No AI was hurt while writing this blog post. Human tears only.
In September 2026 we introduced NGINX 1.31.5 with several core features for one same goal. We expanded the core routing methods and the most critical nginx directives to enable native, non-scripted routing of any API traffic. In this blog post we will go through the most important feature that we called “predicate locations”, and explain its interaction with the rest of the advancements we’ve been making to support modern applications.
Problem Statement
In the ideal world, HTTP is a great protocol. When used correctly, it enables extremely fast and reliable communication that can be both readable by a human and programmable for clients and servers.
HTTP interaction starts with a request line like this:
POST /api/v1/coffee HTTP/1.1
The methods (POST in the example) is designed to tell the backend server what kind of action to perform.
URLs (/api/v1/coffee) are designed to point to the resources.
The headers that follow the request line are designed to instruct the server on various detailed operations. The request body contains the payload.
Routing of HTTP requests is usually designed around the URLs, and in some cases, the methods.
Application engineers ignore the conventions of the protocol and design the traffic modifiers inside the headers and body.
As a result, web servers, proxies, security devices, and load balancers, struggle to efficiently route traffic. They are not designed for application data in the wrong places.
NGINX Modernization: Predicate Locations
NGINX, like any other middle proxy, was designed around URLs.
In NGINX 1.31.5, we removed this limitation and allowed any variable to become the traffic modifier for any location block of the configuration.
Traditional Location Blocks
NGINX configuration structure is based heavily on location blocks. As an illustrative (not literal) example, see this structure:
http {
server {
location / { ... }
location /images { ... }
location /api { ... }
location /api/v1/public { ... }
location /api/v1/configuration { ... }
location /api/v1/coffee { ... }
}
}
This structure allows to put the most meaningful and actionable directives in these location blocks. You can designate different backend server pools (upstreams) for routing, configure various levels of rate and request limiters, modify headers for each direction, set up authentication, WAFs, and other security tools – all independently for each location.
We cannot redefine this base structure. It is too powerful, stable, configurable. Most NGINX users enjoy it.
Predicate Location Blocks
With the expansion of locations to support any variable, the typical location structure becomes more advanced. For example:
http {
server {
location / { ... }
location /images { ... }
location $post_requests { ... }
location $bot_traffic { ... }
location $internal_tests { ... }
location $ai_mcp_queries { ... }
}
}
NGINX evaluates variables. When the variable is found to be true (or rather “anything but false”), you’re in the location.
Do whatever you like with this traffic.
Mapping Predicates
NGINX has a well-known map module. Mapping of variables to values becomes crucial for predicate logic.
Let’s say, you have a list of HTTP methods that you want to evaluate as “true” for use in a predicate location. All other methods should be false. Use this simple example in your configuration:
map $request_method $restricted_methods {
POST 1;
PUT 1;
DELETE 1;
default 0;
}
Setting up default value to “0” is not necessary, default is always “” that evaluates to false. However, explicit declaration can be useful for troubleshooting.
In this example, you now can use the $restricted_methods variable in other places in your configuration, including the predicate locations.
Nesting Predicates
A configuration full of predicates might become quite cumbersome.
If the predicate evaluation is complex in nature, for example if they read the request body, performance might drop.
For these reasons, make your location structure fit your application.
Predicate locations support nesting together with classic URL-based locations. We do not prescribe how you should nest these locations. Your own application has your own traffic patterns that will make sense.
In the example below we use the URIs first but make further traffic distinction based on the predicates:
http {
server {
location /images { ... }
location /api/v1 {
location $restricted_requests {
location $post_requests { ... }
location $bot_traffic { ... }
}
location $internal_tests { ... }
location $ai_mcp_queries { ... }
}
}
}
As an alternative approach, you can define your predicates first but have the conventional locations on the next level:
http {
server {
location $public_methods { ... }
location $restricted_methods {
location /api/v1/private {
location $internal_ips { ... }
location $bot_traffic { ... }
}
location /api { ... }
location /wp-admin { ... }
}
}
}
Routing on HTTP Request Body and JSON Payload
Predicates are natively designed to work with the other new features of NGINX – early reading of the HTTP request body and native parsing of JSON payloads.
JSON is the standard format for modern APIs. JSONPath is the standard to find the values in the JSON payloads.
Now you can set variables based on the fields of the JSON. Then use those variables as predicates in locations.
As an example, let’s route traffic based on one or zero in “vip” field of this JSON payload:
{
"order_id": "1042",
"item": "latte",
"size": "medium",
"user": {
"name": "Nick",
"params": {
"vip": 1
}
}
}
We will use the following NGINX configuration:
events { }
http {
client_body_early_read on; # Required for routing on the body
json_set $vip $request_body user.params.vip;
server {
location $vip {
return 200 "VIP user\n";
}
location / {
return 200 "Regular user, applying restrictions\n";
# limit_req ...;
}
}
}
Test it with curl:
nick:~$ curl -X POST -d '{"order_id":"1042","item":"latte","size":"medium","user":{"name":"Somebody","params":{"vip":0}}}' 127.1:8085
Regular user, applying restrictions
nick:~$ curl -X POST -d '{"order_id":"1042","item":"latte","size":"medium","user":{"name":"Nick","params":{"vip":1}}}' 127.1:8085
VIP user
This configuration showcases something that was not available before: simplicity. We used industry-standard tools, created a variable, and routed directly on it.
JSON parsing and setting of variables is not limited to the request body. You can take any NGINX variable, such as an HTTP header, as a source and parse it.
What can you do further with this example of routing? Send traffic to different upstream servers, limit it by rate, number of requests, or IP addresses. Modify the headers and payload. Apply restrictions based on content, enable or disable application firewalls. The entirety of NGINX feature set is now available through an extremely simple interface.
Complex Routing with NJS
NJS can perform a lot of functions. It can take over the NGINX routing layer completely. However, if you overuse NJS in your NGINX configuration, it might become cumbersome. Also, before the introduction of predicates it was hard to get out of NJS back into NGINX locations. Our users had to write megabytes of NJS code just for routing.
With predicates, NJS becomes simplified.
In the next example we will count the number of words in the HTTP request body, make a decision if the request is too long, and route traffic differently based on that decision. We will use “client_body_early_read” directive to create the “$request_body” variable early in the process, use JavaScript to count the words and set a predicate “$many_words“. Then we will use it in the location directive.
JavaScript file http.js:
function counter(r) {
const words = r.variables['request_body'].trim().split(/\s+/).length;
if (words > 10) {
return 1;
}
return 0;
}
export default {counter};
NGINX configuration file nginx.conf:
events { }
http {
client_body_early_read on;
js_import /path/to/conf/http.js;
js_engine qjs;
js_set $many_tokens http.counter;
server {
location $many_tokens {
return 413 "Request body has too many tokens\n";
}
location / {
return 200 "Request is small, OK\n";
}
}
}
Testing with curl:
nick:~$ curl -X POST -d "a b c d e f g h j k l" 127.1:8085
Request body has too many tokens
nick:~$ curl -X POST -d "a b c d" 127.1:8085
Request is small, OK
As you can see, we avoided creating a complicated set of redirects, natively enabled all nginx features through direct use of location directives, and used the full power of JavaScript for a specific small task.
Future: Native Complex Conditions
We are working on adding native support for complex conditional logic inside NGINX. The goal is to make it easier to handle diverse traffic scenarios with precision and efficiency, keeping nginx configuration simple and reliable.
You will be able to provide complex logical conditions very similar to the current “if” directive but without the caveats and restrictions.
This feature will be available as a separate directive that will evaluate to true/false depending on the condition. You will use it in predicate locations or in other places where the true/false logic is needed.
Tips, Tricks, Caveats
When your configuration is heavy on predicates, use these guidelines:
Create catch-all locations. In standard configs, a regular “location /” block only makes sense in the top level of nesting. With predicates it makes sense on any nesting level as the fallback/catch-all. Place it in the bottom of the block.
Limit early body reading only to areas where it is truly needed. Larger request bodies take a lot of memory and CPU for processing. The directive “
client_body_early_read” supports variables. Use predicates on HTTP headers or URLs to selectively enable it. When you can, of course.Pay extra attention on the order of predicates in the configuration. For performance reasons, place simple and popular predicate locations on the top of the list and more sophisticated ones towards the bottom.
Name your predicates appropriately so you can track and troubleshoot them effectively later.
Use special log formats to track the variables. Your logs can show the values of multiple variables in the same line. It will help with troubleshooting.
Conclusion
NGINX now allows you to route traffic on almost anything. We created a set of features that harmoniously work together:
Predicate locations
Early reading of request body
Native parsing of JSON payloads
All of these features are building blocks. They work seamlessly with the rest of NGINX.
We are not prescribing how your application should look, nor how to organize your configuration, nor how to manage it. With these generic functions and examples you can create your own proxy and a load balancer that fits virtually any traffic pattern.
See the reference documentation on nginx.org:
NGINX Location Blocks: https://nginx.org/en/docs/http/ngx_http_core_module.html#location
NGINX JSON Module: https://nginx.org/en/docs/http/ngx_http_json_module.html
NGINX
client_body_early_readDirective: https://nginx.org/en/docs/http/ngx_http_core_module.html#client_body_early_readNGINX 1.31.5 Release Blog: https://blog.nginx.org/blog/nginx-1-31-5-control-api-predicate-locations-early-body-inspection-and-more
Change Log for NGINX: https://nginx.org/en/CHANGES


