Almost no Node process should face the internet directly. A reverse proxy — nginx 75 , Caddy 7,400 , HAProxy 6,072 or a cloud load balancer — terminates TLS, serves static files without waking your event loop, compresses responses, buffers slow clients, and spreads traffic over several app instances.
upstream api { server 127.0.0.1:3000; keepalive 64; } # pooled connections to Node
server {
listen 443 ssl http2;
server_name api.example.com;
gzip on; gzip_types application/json text/css application/javascript;
location / {
proxy_pass http://api;
proxy_http_version 1.1; # 1.0 is the default and disables keep-alive
proxy_set_header Connection "";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}The two lines people forget are proxy_http_version 1.1 and the empty Connection header; without them nginx opens a fresh TCP connection to Node for every request. Measuring that against the cached server from Caching and Redis — 4,000 requests at concurrency 16 over loopback, same 18-core i9-7980XE, Node 25.8.0 — shows the cost.
| Client agent | Throughput | Time per request |
|---|---|---|
| new http.Agent({ keepAlive: false }) | 2,773 req/s | 0.361 ms |
| new http.Agent({ keepAlive: true, maxSockets: 16 }) | 11,814 req/s | 0.085 ms |
Four times the throughput for one option, and this is the cheapest possible case: no TLS handshake, no network latency, no packet loss. Across a real network the ratio grows, because every new connection pays a round trip for the TCP handshake and several more for TLS. The same applies to calls your service makes outward, where undici 7,705 's pool (Making HTTP Requests from Node) does this job.
On the Node side, trust the proxy's headers only when the proxy really is in front of you: X-Forwarded-For is client-supplied text on a directly reachable port, and a spoofed value poisons your rate limiter. Bind to 127.0.0.1, and use Express 24,430 's trust proxy setting (Express.js) instead of reading the header by hand. Keep Node's server.keepAliveTimeout above the proxy's idle timeout — if Node closes first, the proxy can send a request into a socket that is already going away and return a 502 to a blameless user.
The order matters more than any single technique. Measure a real workload, fix what the measurement points at, re-measure to confirm, and only then reach for another process, another cache or another machine. A server that answers in two milliseconds on one core is worth more than eighteen cores hiding a JSON.parse that never needed to run.