NGINX as Reverse Proxy
New to NGINX? Start by reading NGINX introduction.
Notes
a reverse proxy is a server that receives requests from clients and forwards those requests to another server (called the upstream or backend), then returns the backend’s response to the client
without a reverse proxy, clients can directly access the backend application:
Client | v Backend :8000with NGINX acting as a reverse proxy, clients communicate with NGINX instead, and NGINX communicates with the backend:
Client | v NGINX :80 | v Backend :8000the client does not need to know where or on which port the backend application is running
this is useful because NGINX can handle responsibilities that you generally don’t want every backend application to implement itself, such as:
- SSL/TLS termination — NGINX handles HTTPS and TLS certificates while the backend can communicate using plain HTTP internally
- rate limiting — limit how many requests a client can make within a period of time
- WAF integration — inspect and filter potentially malicious HTTP requests before they reach the application
- authentication/access control — restrict access to certain applications or paths before forwarding requests
- load balancing — distribute requests between multiple backend servers
- caching — cache responses so that repeated requests don’t always reach the backend
- compression — compress responses before sending them to clients
- logging — maintain centralized access and error logs regardless of which backend handles the request
- hiding backend infrastructure — clients only interact with NGINX rather than directly accessing internal application servers
reverse proxying also provides a clean separation between the public-facing web server and the application server:
Public Network | v +---------------+ | NGINX | | :80/:443 | +---------------+ | Private Network | v +---------------+ | Application | | :8000 | +---------------+for example, a Python application may listen on
localhost:8000, while NGINX listens on port80and exposes the application to clientsproxy_passis the NGINX directive used to forward requests to another server:location / { proxy_pass http://localhost:8000; }the backend does not necessarily have to be running on the same machine; it can be another server:
location / { proxy_pass http://192.168.1.20:8000; }NGINX can therefore act as a single entry point for multiple applications:
NGINX | +-------------+-------------+ | | | v v v frontend API server admin app :3000 :8000 :9000one important consequence is that backend applications usually should not be exposed directly to the public internet. They can listen on private addresses or
localhost, while NGINX is the only publicly accessible server.
Setting Up a Reverse Proxy
The general configuration process is similar to serving static content. First create a site configuration under
/etc/nginx/sites-available/:sudo nano /etc/nginx/sites-available/mysite.comThe basic reverse proxy configuration is:
server { listen 8090; location / { proxy_pass http://localhost:8000; } }When a request arrives at
mysite.com:8090/, NGINX forwards the request tolocalhost:8000.This means some application must actually be listening on
localhost:8000.For example, a simple Python HTTP server:
from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): def do_GET(self): message = "Hello From Python Backend!" self.send_response(200) self.send_header("Content-Type", "text/plain") self.end_headers() self.wfile.write(message.encode()) server = HTTPServer(("localhost", 8000), Handler) print("Server running at http://localhost:8000") server.serve_forever()Run it with:
python3 server.pyIf Python is not already installed:
sudo apt update sudo apt install python3Before involving NGINX, make sure the backend itself works:
curl localhost:8000It should return:
Hello From Python Backend!Now create the NGINX reverse proxy configuration:
server { listen 8090; location / { proxy_pass http://localhost:8000; } }Enable the site by creating a symlink:
sudo ln -s /etc/nginx/sites-available/mysite.com /etc/nginx/sites-enabled/Validate the NGINX configuration before starting or reloading NGINX:
sudo nginx -tIf the configuration is valid, you should see something similar to:
syntax is ok test is successfulStart NGINX if it is not already running:
sudo nginxIf NGINX is already running and you have only changed the configuration, reload it instead:
sudo nginx -s reloadNow access NGINX instead of the backend:
curl localhost:8090You should still receive:
Hello From Python Backend!The request flow is now:
curl localhost:8090 | v NGINX :8090 | proxy_pass | v Python :8000 | v "Hello From Python Backend!"The important point is that
curlnever communicates with the Python application directly. NGINX receives the request and forwards it to the Python server.
Using Node.js as the Backend
instead of Python, any HTTP application can be used as the backend
for example, a minimal Node.js HTTP server:
const http = require('http'); http.createServer((req, res) => { res.end('Hello From Node.js Backend!'); }).listen(3000);run it with:
node server.jsif Node.js is not installed:
sudo apt update sudo apt install nodejs npm -ychange the NGINX
proxy_passto point to the Node.js application:server { listen 8090; location / { proxy_pass http://localhost: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; } }after changing the configuration:
sudo nginx -t && sudo nginx -s reloadnow:
curl localhost:8090should return:
Hello From Node.js Backend!
Passing Client Information to the Backend
when NGINX proxies a request, it modifies some request headers by default
for example, the
Hostheader is changed to the host specified by theproxy_passURL by defaultthis can be undesirable because the backend may need to know the original host requested by the client
NGINX can explicitly forward useful client information using
proxy_set_header:server { listen 8090; location / { proxy_pass http://localhost:8000; 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; } }Host $hostpasses the original hostname requested by the clientX-Real-IP $remote_addrpasses the client’s IP addressX-Forwarded-For $proxy_add_x_forwarded_formaintains the chain of client IP addresses when there are multiple proxiesX-Forwarded-Proto $schemetells the backend whether the original request usedhttporhttpsthese headers are especially important when the backend needs to perform things such as logging, redirects, authentication, or generating absolute URLs
Managing Reverse Proxy Buffers
when NGINX acts as a reverse proxy, there are two separate connections involved:
Client | | 1. request v NGINX | | 2. request v Backend | | 3. response v NGINX | | 4. response v Clientthe backend may be able to generate a response much faster than the client can download it
for example, suppose the backend generates a
10 MBresponse in one second:Backend | | generates 10 MB in 1 second v NGINX | | client downloads at 1 MB/s v Client Backend finished: ~1 second Client finished: ~10 secondswithout response buffering, NGINX passes the response to the client as it receives it from the backend
this means the backend’s connection can remain tied to the slow client for the entire time the client is downloading the response
with buffering enabled, NGINX can receive the response from the backend quickly and store it in memory buffers while the client downloads it at its own speed:
fast Backend -----------------> NGINX | | stores response | in buffers | | slow v Clientthis allows the backend to finish processing the request and become available for another request while NGINX continues delivering the response to the slow client
NGINX enables response buffering by default. The relevant directives are
proxy_buffering,proxy_buffers, andproxy_buffer_size.
Why Buffering Helps Slow Clients
imagine an application server has
100workers availablewithout buffering, if
100clients are connected and each client is very slow, those workers may remain occupied while the clients slowly download their responses:Backend workers [busy] [busy] [busy] [busy] ... [busy] ^ ^ ^ ^ | | | | slow slow slow slow clientsthe backend may have already generated the responses, but its connections remain occupied because the clients are consuming the responses slowly
with NGINX buffering:
Backend | | quickly generates response v NGINX buffers | | slowly sends response v Clientthe backend can finish the request sooner and release its resources
this is especially useful when NGINX is sitting in front of application servers where backend workers, connections, or other resources are expensive
therefore, buffering essentially decouples the speed of the backend from the speed of the client
Without buffering: Backend speed <-----------------> Client speed tightly coupled With buffering: Backend speed ---> NGINX buffer ---> Client speed independent independent
How Do You Know a Client Is Slow?
there is no simple
slow_client = trueproperty that you normally inspectinstead, think about how quickly the backend produces the response compared with how quickly the client consumes it
a client may effectively be slow because of:
- a poor or unstable internet connection
- low available bandwidth
- a mobile connection
- a geographically distant client
- a large response being downloaded
- a client application that reads the response slowly
- a client intentionally throttling its download speed
for example, if the backend generates a response in
200 msbut the client takes30 secondsto receive it, the client is effectively slow relative to the backendyou can investigate this behavior through NGINX access logs and monitoring
useful timing information includes:
- how long the upstream takes to generate a response
- how long the complete request takes
- response size
- request rate
- network throughput
NGINX provides variables such as
$request_timeand$upstream_response_timethat can be included in access logs:log_format timing '$remote_addr "$request" ' 'status=$status ' 'request_time=$request_time ' 'upstream_response_time=$upstream_response_time ' 'bytes_sent=$bytes_sent'; access_log /var/log/nginx/mysite.com/access.log timing;for example, you might see:
request_time=30.2 upstream_response_time=0.2this indicates that the upstream generated its response in roughly
0.2seconds, while the entire request took roughly30.2secondsthis does not by itself prove that the client is slow because network and other factors can contribute, but a large difference between total request time and upstream response time can be a useful indication that the response spent significant time being delivered after the upstream had finished
When Should Buffering Be Used?
response buffering is generally a good default for ordinary web applications and APIs, especially when:
- backend responses are generated quickly
- responses can be relatively large
- clients may have different network speeds
- backend worker/connection resources are valuable
- you want NGINX to absorb differences between backend and client speeds
for example:
Fast API | | response generated quickly v NGINX | | buffers response v Slow clientthis allows the API server to move on to other requests instead of remaining tied to the slow client
Configuring Response Buffering
NGINX response buffering is controlled primarily through
proxy_buffering:location / { proxy_pass http://localhost:8000; proxy_buffering on; }onis the default, so explicitly specifying it is usually unnecessary:location / { proxy_pass http://localhost:8000; }the amount and number of buffers can be controlled using
proxy_buffers:location / { proxy_pass http://localhost:8000; proxy_buffers 16 4k; }the syntax is:
proxy_buffers number size;therefore:
proxy_buffers 16 4k;means NGINX allocates
16buffers of4 KBeach for a request.there is also a separate buffer for the beginning of the response, which normally contains the response headers. Its size can be configured using
proxy_buffer_size:location / { proxy_pass http://localhost:8000; proxy_buffer_size 2k; proxy_buffers 16 4k; }NGINX’s documentation uses the same type of configuration, where
proxy_bufferscontrols the number and size of buffers andproxy_buffer_sizecontrols the buffer used for the initial portion of the response.
When Should Buffering Be Disabled?
buffering is not always desirable
some applications need the client to start receiving data immediately as the backend generates it
examples include:
- streaming responses
- Server-Sent Events (SSE)
- some long-lived HTTP responses
- applications where incremental output must reach the client immediately
in these situations, buffering can interfere with the desired streaming behavior because NGINX normally stores the proxied response before sending it onward
buffering can be disabled for a particular location:
location /stream/ { proxy_pass http://localhost:8000; proxy_buffering off; }with buffering disabled, NGINX sends the response to the client synchronously as it receives it from the backend.
Backend | | response chunk v NGINX | | immediately forwards v Clientthis is useful when low-latency delivery of each piece of the response is more important than protecting the backend from slow clients
Buffering vs. No Buffering
Buffering ON
------------
Backend
|
| fast response
v
NGINX
|
| buffer
|
+--------------------+
| |
v v
fast client slow clientBuffering OFF
-------------
Backend
|
| response data
v
NGINX
|
| immediately forwards
v
Client
Backend and client remain
closely coupled.therefore, a useful rule of thumb is:
Normal web application/API | v buffering ON (default) Streaming / incremental response | v consider buffering OFF
Tip
Do not increase buffer sizes simply because a server has more RAM. Larger buffers consume more memory per request and may become expensive when many requests are handled concurrently. Tune buffering based on the response sizes, concurrency, backend behavior, and client behavior you actually observe.
Important
Buffering does not make a slow client’s internet connection faster. Its main benefit is that NGINX can absorb the difference between a fast backend and a slow client, allowing the backend to finish its work without remaining tied to the client’s download speed.
NGINX as the Public Entry Point
in a more realistic setup, the backend application should generally listen only on
localhostor a private network interface:Internet | v NGINX :80/:443 | +----> Python :8000 | +----> Node.js :3000 | +----> another application :9000clients only need to know the public NGINX address
NGINX can then decide where each request should go based on the hostname or URL path:
server { listen 80; server_name example.com; location / { proxy_pass http://localhost:8000; } location /api/ { proxy_pass http://localhost:3000; } }this allows a single NGINX server to expose multiple applications through a single public endpoint while keeping the applications themselves behind the reverse proxy
Important
A reverse proxy is not itself the backend application. NGINX receives the client request, optionally processes it, forwards it to the backend, receives the backend response, and sends that response back to the client.
Tip
One of the biggest benefits of a reverse proxy is that backend applications can focus on application logic while NGINX handles common HTTP infrastructure concerns such as TLS termination, rate limiting, request filtering, caching, compression, and load balancing.