Adding bot protection to an application usually means editing application code: middleware, decorators, request hooks. That works, but it scatters the enforcement logic across services and couples it to your framework. The auth_request module in nginx offers a cleaner seam. It lets the reverse proxy consult a decision service before it forwards a request upstream, centralizing the gate at the edge where all traffic already passes.
This article shows how to build a bot gate with auth_request that forwards device-intelligence signals to a decision endpoint, caches the verdict, and — critically — fails open so a detector outage never takes down your site. It pairs naturally with edge bot detection on Cloudflare for teams that want protection before the origin.
How auth_request works
The auth_request directive tells nginx to issue an internal subrequest to a named location before processing the real request. The status code of that subrequest decides the outcome: a 2xx allows the request to proceed upstream, while 401 or 403 stops it. Everything else is treated as an error you handle.
The mental model is a gate in front of your location block. Every matching request first knocks on the auth endpoint. The endpoint inspects whatever signals nginx forwards, makes a decision, and answers with a status code. nginx enforces that answer.
location /checkout {
auth_request /_bot_gate;
# if the subrequest returns 2xx, proceed:
proxy_pass http://app_upstream;
}
location = /_bot_gate {
internal;
proxy_pass http://prynt_decision;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Original-URI $request_uri;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Prynt-Token $http_x_prynt_token;
}
The internal directive makes /_bot_gate unreachable from outside; only nginx subrequests can hit it. proxy_pass_request_body off means the gate sees headers and metadata, not the full payload, which keeps it fast. This mirrors the pattern used in framework-level integrations like Node server-side verification.
Forwarding the right signals
The decision endpoint can only be as smart as the signals it receives. The subrequest carries whatever headers you set, so forward the evidence the detector needs to make a call.
- The device token or visitor ID, typically set by the client SDK and sent as a header or cookie. This is the primary key for a device-level decision.
- The true client IP, so the detector can classify the network. See datacenter IP detection and proxy detection. Only trust forwarded-for headers from proxies you control.
- The original URI and method, so the gate can apply route-specific policy — stricter on login and checkout, looser on public reads.
- TLS characteristics where your build exposes them, feeding JA4 fingerprinting.
The decision service resolves these into a verdict: allow, block, or challenge. For a confidence score with reason codes, the endpoint can also return headers that nginx passes to the upstream, so your application knows why a request was allowed and can apply its own graduated response.
Caching the decision
A subrequest per request adds a round trip, and you do not want to re-evaluate the same device on every asset load. Caching the verdict is what makes the gate practical at scale.
Two complementary layers:
- nginx-side caching of the subrequest response keyed on the device token, so repeated requests from the same known device reuse the decision for a short TTL.
- Decision-service caching of the underlying device evaluation, so even a cache miss at the proxy does not recompute the full signal set.
location = /_bot_gate {
internal;
proxy_pass http://prynt_decision;
proxy_cache gate_cache;
proxy_cache_key "$http_x_prynt_token";
proxy_cache_valid 200 403 30s;
proxy_set_header X-Real-IP $remote_addr;
}
Keep TTLs short. A device’s risk can change within a session — a token replayed from a new network should not ride a stale allow. Balance the round-trip savings against how quickly you need to react to a device turning malicious. Scoping the gate to sensitive routes, rather than every request, is often the bigger win; static assets rarely need a bot decision.
Failing open versus failing closed
The most important design decision is what happens when the detection service is slow or down. auth_request treats a 5xx or a timeout as an error, and by default an error blocks the request. For most sites, that is the wrong default: an outage in your fraud detector should not become an outage for every customer.
Use error_page to define the behavior explicitly.
location /checkout {
auth_request /_bot_gate;
error_page 500 502 503 504 = @allow_on_gate_failure;
proxy_pass http://app_upstream;
}
location @allow_on_gate_failure {
proxy_pass http://app_upstream; # fail open
}
The trade-off:
| Failure mode | Behavior on outage | Fits |
|---|---|---|
| Fail open | Allow traffic, log the gap | Most consumer sites, checkout, content |
| Fail closed | Block until detector recovers | High-value targets under active attack |
Set a short proxy_read_timeout on the gate so a hung detector does not stall requests waiting to fail open. For most teams, fail open with alerting is correct: you would rather serve a few extra bots during a rare outage than block every real user. Reserve fail-closed for endpoints where the cost of letting a bot through exceeds the cost of downtime.
Operating the gate
Once the gate is live, treat it as production infrastructure:
- Roll out on one route first — usually login or checkout — before widening. See protecting a login form and rate limiting by device.
- Log the verdict and reason codes so you can audit blocks and tune thresholds against a false-positive budget.
- Monitor gate latency and error rate as first-class metrics; a slow gate is a slow site.
- Keep the detector local. Running the decision service in the same network as nginx — the self-hosted model — keeps the subrequest fast and the signals inside your boundary. See why self-host fraud detection.
- Combine with app-level checks for defense in depth; the gate is a coarse first filter, not the whole bot detection program.
Frequently asked questions
What does nginx auth_request do?
It issues an internal subrequest to an authorization endpoint before serving the main request. A 2xx from that endpoint allows the request; a 401 or 403 blocks it. This makes nginx a centralized gate.
Does auth_request add latency to every request?
It adds one subrequest round trip. Caching decisions per device or session and keeping the auth endpoint local keeps the added latency small, and you can scope the gate to sensitive routes only.
What happens if the detection service goes down?
You choose the failure mode with error_page handling. For most sites, failing open — allowing traffic when the detector is unreachable — is safer than blocking every user during an outage.
The auth_request module turns nginx into a clean, centralized bot gate that lives at the edge instead of scattered through your application. Forward the device and network signals, cache the verdict with a short TTL, scope the gate to routes that matter, and fail open unless you are actively under siege. Wire it to a self-hosted decision service and you get edge enforcement without shipping your users’ data to anyone. Explore the SDKs and docs to build the decision endpoint.
Run it yourself
Prynt is open-source, self-hostable device intelligence — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics you own end to end.