Skip to content

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 :8000
  • with NGINX acting as a reverse proxy, clients communicate with NGINX instead, and NGINX communicates with the backend:

    Client
       |
       v
    NGINX :80
       |
       v
    Backend :8000
  • the 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 port 80 and exposes the application to clients

  • proxy_pass is 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          :9000
  • one 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

  1. 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.com
  2. The 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 to localhost:8000.

  3. 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.py

    If Python is not already installed:

    sudo apt update
    sudo apt install python3
  4. Before involving NGINX, make sure the backend itself works:

    curl localhost:8000

    It should return:

    Hello From Python Backend!
  5. Now create the NGINX reverse proxy configuration:

    server {
        listen 8090;
    
        location / {
            proxy_pass http://localhost:8000;
        }
    }
  6. Enable the site by creating a symlink:

    sudo ln -s /etc/nginx/sites-available/mysite.com /etc/nginx/sites-enabled/
  7. Validate the NGINX configuration before starting or reloading NGINX:

    sudo nginx -t

    If the configuration is valid, you should see something similar to:

    syntax is ok
    test is successful
  8. Start NGINX if it is not already running:

    sudo nginx

    If NGINX is already running and you have only changed the configuration, reload it instead:

    sudo nginx -s reload
  9. Now access NGINX instead of the backend:

    curl localhost:8090

    You 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 curl never 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.js
  • if Node.js is not installed:

    sudo apt update
    sudo apt install nodejs npm -y
  • change the NGINX proxy_pass to 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 reload
  • now:

    curl localhost:8090

    should 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 Host header is changed to the host specified by the proxy_pass URL by default

  • this 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 $host passes the original hostname requested by the client

  • X-Real-IP $remote_addr passes the client’s IP address

  • X-Forwarded-For $proxy_add_x_forwarded_for maintains the chain of client IP addresses when there are multiple proxies

  • X-Forwarded-Proto $scheme tells the backend whether the original request used http or https

  • these 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
    Client
  • the backend may be able to generate a response much faster than the client can download it

  • for example, suppose the backend generates a 10 MB response 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 seconds
  • without 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
                                Client
  • this 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, and proxy_buffer_size.

Why Buffering Helps Slow Clients

  • imagine an application server has 100 workers available

  • without buffering, if 100 clients 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
    clients
  • the 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
    Client
  • the 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 = true property that you normally inspect

  • instead, 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 ms but the client takes 30 seconds to receive it, the client is effectively slow relative to the backend

  • you 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_time and $upstream_response_time that 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.2
  • this indicates that the upstream generated its response in roughly 0.2 seconds, while the entire request took roughly 30.2 seconds

  • this 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 client
  • this 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;
    }
  • on is 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 16 buffers of 4 KB each 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_buffers controls the number and size of buffers and proxy_buffer_size controls 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
    Client
  • this 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 client
Buffering 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 localhost or a private network interface:

    Internet
        |
        v
    NGINX :80/:443
        |
        +----> Python :8000
        |
        +----> Node.js :3000
        |
        +----> another application :9000
  • clients 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.

Last updated on