nginx reverse proxy
Configure nginx as a reverse proxy to route crawler traffic through Edge Gateway for server-side rendering of JavaScript-heavy pages.
Prerequisites
- Running Edge Gateway instance (see Quick Start)
- Configured host with
render_keyanddomain - nginx 1.14+ with
ngx_http_proxy_module
How it works
nginx sits between clients and your origin server. When a crawler requests a page, nginx routes the request to Edge Gateway for pre-rendered HTML. Regular users go directly to your origin server, preserving cookies, sessions, and authentication.
Edge Gateway endpoint: GET /render?url=<target-url>
Required header: X-Render-Key (from your host configuration)
Map configuration
Maintain detection logic in a separate file and include it in the http context (map directives cannot be inside a server block). This configuration catches crawlers using generic keywords plus explicit patterns for crawlers without these keywords in their name.
# 1. Detect crawlers by User-Agent
map $http_user_agent $ec_crawler {
default 0;
# Generic crawler keywords
"~*bot" 1;
"~*crawl" 1;
"~*spider" 1;
"~*slurp" 1;
# Crawlers without generic keywords in name
"~*WhatsApp" 1;
"~*Snapchat" 1;
"~*facebookexternalhit" 1;
"~*AMZN-User" 1;
"~*Claude-User" 1;
"~*Perplexity-User" 1;
"~*ChatGPT-User" 1;
}
# 2. Skip static assets (inherits $ec_crawler, disables for static files)
map $uri $ec_skip_render {
default $ec_crawler;
"~*\.(avif|css|eot|gif|gz|ico|jpeg|jpg|js|json|map|mp3|mp4|ogg|otf|pdf|png|svg|ttf|txt|wasm|wav|webm|webp|woff|woff2|xml|zip)$" 0;
}
# 3. Loop prevention (inherits $ec_skip_render, disables for EdgeComet callbacks: render and bypass fetches)
map $http_x_edge_render $ec_should_render {
default $ec_skip_render;
"~." 0;
}For alternative crawler detection approaches and detailed configuration explanations, see the nginx reference.
Server configuration
Use the $ec_should_render variable from the map configuration to route crawler traffic.
upstream rendergw {
server 127.0.0.1:10070;
# Reuse connections instead of handshaking on every crawler request
keepalive 8;
}
# Map directives require http context (outside server block)
include conf.d/edge-comet-map.conf;
server {
listen 80;
server_name example.com;
location / {
# Route crawlers to Edge Gateway (logic computed in maps above)
error_page 418 = @edge_render;
if ($ec_should_render = 1) {
return 418;
}
# ... your existing proxy configuration ...
}
location @edge_render {
internal;
proxy_pass http://rendergw/render?url=$scheme://$host$request_uri;
# Connection reuse requires HTTP/1.1 and an empty Connection header.
# Both are defaults since nginx 1.29.7 and required before it.
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header X-Render-Key "your_render_key_here";
proxy_set_header User-Agent $http_user_agent;
# Forward original client details
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;
proxy_set_header EC-Request-ID $request_id;
# Timeouts should exceed EG render timeout
proxy_connect_timeout 10s;
proxy_read_timeout 60s;
proxy_send_timeout 10s;
}
}Replace:
127.0.0.1:3000with your origin server address.127.0.0.1:10070with your Edge Gateway address.your_render_key_herewith your host'srender_key.example.comwith your domain.
Connection reuse
nginx pools connections to Edge Gateway only when the upstream block enables keepalive and the proxy speaks HTTP/1.1 with an empty Connection header. None of that is the default before nginx 1.29.7, so on those versions every crawler request opens a new connection.
Crawlers fetch in bursts, so the pool stays warm exactly when it matters: the first request of a burst still handshakes, the rest reuse the connection.
The saving is one TCP round trip for a local Edge Gateway, and a TCP plus TLS handshake for a remote one. It shows up on cache hits, where the handshake is a large share of total response time, and disappears into the noise on fresh renders that take seconds.
Idle connections must be closed by nginx rather than by the other end, otherwise nginx can send a request onto a connection that is already being torn down. Keep keepalive_timeout in the upstream block below the idle timeout of whatever nginx connects to:
| Upstream | nginx keepalive_timeout | Why |
|---|---|---|
| Edge Gateway directly | leave the 60s default | EG idle timeout equals server.timeout (120s in the sample config). |
| An HTTPS endpoint behind a CDN | below the CDN idle limit | Cloudflare closes idle client connections at 400s, so 300s is safe. |
For an HTTPS upstream, also set proxy_ssl_server_name on; so SNI is sent.
See the nginx reference for version requirements and verification.
Origin failover
Serve the page from your origin when Edge Gateway is unreachable or returns a server error, so a rendering outage does not turn into an outage for crawlers.
server {
location / {
# Required: routing to @edge_render already spends one error_page
# redirect, and the failover below needs a second one
recursive_error_pages on;
error_page 418 = @edge_render;
if ($ec_should_render = 1) {
return 418;
}
# ... your existing proxy configuration ...
}
location @edge_render {
internal;
# ... proxy_pass and headers as above ...
proxy_intercept_errors on;
error_page 500 502 503 504 = @origin_fallback;
proxy_connect_timeout 3s;
}
location @origin_fallback {
internal;
# Mirror the origin configuration from "location /"
proxy_pass http://127.0.0.1:3000;
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;
}
}This covers both outage modes. An unreachable Edge Gateway makes nginx generate its own 502 or 504, and proxy_connect_timeout 3s keeps that detection fast. An Edge Gateway that responds with 5xx is intercepted by proxy_intercept_errors and re-routed the same way.
Only the codes listed in error_page are intercepted, so status actions (403, 404, 410), redirects, and origin 404s relayed through bypass still reach the crawler unchanged.
Two limits are worth knowing. An Edge Gateway that accepts the connection but never responds fails over only after proxy_read_timeout, which cannot be shortened without cutting off legitimate long renders. And if your origin itself returns a 5xx through bypass mode, nginx intercepts it and fetches the origin a second time, which costs one extra request on an already failing page.
Verifying the setup
Test crawler detection
Send a request with a crawler User-Agent:
curl -v \
-H "User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" \
"http://example.com/"Check response headers:
EC-Source: renderorEC-Source: render_cacheconfirms EG processed the request.
Test regular user
Send a request with a browser User-Agent:
curl -v \
-H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36" \
"http://example.com/"Request should go directly to origin with no EC-* headers in response.
Debug crawler detection
Add a temporary header to see the final rendering decision:
add_header X-EC-Should-Render $ec_should_render;Test connection reuse
Add $upstream_connect_time to your log format (see the nginx reference) and watch crawler requests. Reused connections log connect=0.000, while the first request after an idle gap still shows the handshake cost.
Test origin failover
Stop Edge Gateway and send a crawler request. You should get your origin page with a 200 status and no EC-* headers, not a 502.
Troubleshooting
403 Forbidden from Edge Gateway
- Verify
X-Render-Keymatches your host configuration. - Check the domain in the URL matches your configured
domain. - Confirm the host is
enabled: true.
Timeout errors
- Increase
proxy_read_timeoutto exceed your EG render timeout. - Check EG logs for render failures.
- Verify render service is running and registered.
Crawlers not being detected
- Check the value of
$ec_should_renderusing the debug header above. - Add missing patterns to the
$ec_crawlermap block. - Verify User-Agent header is being passed correctly.
Infinite loops
- Ensure
proxy_passin the@edge_renderblock uses variables like$schemeand$hostcorrectly. - Verify that
X-Edge-Renderheader is not being stripped by any other proxy in the chain.
Connections not being reused
- Confirm
proxy_set_header Connection "";is present. Without it nginx sendsConnection: closeon nginx before 1.29.7 and the pool never forms, with no error to show for it. - Confirm
proxy_http_version 1.1;is present on nginx before 1.29.7, where the default is HTTP/1.0. - Check that
keepalivesits inside theupstreamblock. It has no effect next toproxy_pass, and a bareproxy_pass http://hostwithout anupstreamblock cannot pool at all. keepalive_timeoutinsideupstreamrequires nginx 1.15.3+. Older builds reject it at config test.
Failover not reaching the origin
- Confirm
recursive_error_pages on;is set inlocation /or the enclosingserverblock. Routing to@edge_renderalready uses oneerror_pageredirect, so without it the failover redirect is silently ignored and the crawler receives the 502. - Confirm
proxy_intercept_errors on;is set, otherwise upstream 5xx responses pass straight through to the client.
Cache not working
- Verify
EC-Sourceheader showsrender_cacheon repeat requests. - Check
EC-Cache-Ageheader for cache duration. - Review EG cache configuration and storage permissions.
- Ensure nginx itself is not agressively caching the render endpoint (unless configured to respect Vary headers).
Related documentation
- nginx reference - Detailed configuration explanations
- Diagnostic headers - Response header reference
- Dimensions - Crawler detection via User-Agent matching
- Caching - Cache configuration
- Bypass mode - Direct origin fetching