Nginx proxy_pass Trailing Slash Explained

Last updated Sunday, May 17, 2026 12:08 pm Joshua James 9 min read

Nginx proxy_pass path handling is one of the easiest ways to create a reverse proxy that looks correct but sends the wrong URI to the backend. The small difference between proxy_pass http://127.0.0.1:3000; and proxy_pass http://127.0.0.1:3000/; decides whether Nginx keeps the matched location prefix or replaces it before proxying the request.

The safest workflow is to prove the upstream URI in a lab config before editing a production virtual host. If you are still building the base proxy, start with the Nginx reverse proxy configuration guide. If a path already fails after proxying, the related Nginx 404 troubleshooting guide is a useful companion reference.

Understand Nginx proxy_pass Trailing Slash Behavior

The common shortcut is to say “the trailing slash matters,” but the real rule is more precise: proxy_pass can be configured with or without a URI part after the upstream host. When a URI part exists, Nginx replaces the part of the normalized request URI that matched the current location. When no URI part exists, Nginx sends the request URI through without that prefix replacement.

The official Nginx proxy_pass directive documentation describes this as URI replacement. The slash after the upstream host is important because http://backend/ contains the URI /, while http://backend contains no URI part.

Configuration PatternRequestURI Sent UpstreamWhat Happens
location /api/
proxy_pass http://backend;
/api/users/api/usersNginx keeps the matched prefix because proxy_pass has no URI part.
location /api/
proxy_pass http://backend/;
/api/users/usersNginx replaces /api/ with /, so the backend receives the path without the prefix.
location /api/
proxy_pass http://backend/service/;
/api/users/service/usersNginx replaces /api/ with /service/.

This behavior is useful when the public URL and backend URL do not use the same path. For example, a public endpoint at /api/users might need to reach a backend route at /users. In that case, a slash after the upstream host is often correct. If the backend application already expects /api/users, omit the URI part from proxy_pass.

Change proxy_pass path rules during a maintenance window when the location already handles production traffic. A single slash can change every route under that location.

How Nginx Treats Query Strings with proxy_pass

Normal query strings are preserved when Nginx replaces the path. If a client requests /api/users?active=1 and your location strips /api/, the backend receives /users?active=1. That is usually what you want for API routes.

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}

With that configuration, the path mapping works like this:

Client request:  /api/users?active=1
Backend request: /users?active=1

If you use rewrites, variables, or an explicit query string inside proxy_pass, test the final request carefully because those cases can change how the upstream URI is built.

Compare Common Nginx proxy_pass Slash Patterns

The safest way to choose a proxy_pass form is to start from the backend route. Ask what path the application expects before deciding whether Nginx should keep, strip, or replace the public prefix.

Keep the Public Prefix with proxy_pass

Use proxy_pass without a URI part when the upstream application expects the same path the client requested. This is common when the app is mounted under the same prefix on both sides, such as /api/.

location /api/ {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;

    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 keeps the matched prefix:

Client request:  /api/users
Backend request: /api/users

Choose this pattern when the backend framework, router, or API gateway already has routes that begin with /api/.

Strip the Public Prefix with proxy_pass

Use proxy_pass with / after the upstream host when the public prefix is only an Nginx routing prefix and the backend expects paths from the site root.

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
    proxy_http_version 1.1;

    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 strips the matched prefix and sends the remaining path to the backend:

Client request:  /api/users
Backend request: /users

Choose this pattern when Nginx exposes a backend under a public prefix, but the backend app still serves routes from /.

Replace the Public Prefix with a Backend Prefix

The URI part does not have to be only /. You can replace a public prefix with another upstream prefix when the backend uses a different route structure.

location /api/ {
    proxy_pass http://127.0.0.1:3000/backend-api/;
    proxy_http_version 1.1;

    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;
}

The path mapping becomes:

Client request:  /api/users
Backend request: /backend-api/users

Use this only when the backend really owns the replacement prefix. If the upstream application redirects users to its own absolute paths, you may also need proxy_redirect or application-side base URL settings, but do not add those until you have confirmed the path sent upstream.

Redirect the Prefix Without a Trailing Slash

For a slash-terminated proxied prefix such as /api/, Nginx normally returns a 301 redirect from /api to /api/. Add an exact-match block when you want that redirect to be explicit or when /api should do something different.

location = /api {
    return 301 /api/;
}

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
    proxy_http_version 1.1;

    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;
}

The exact block makes the bare-prefix response visible in the configuration. If you do not want the automatic slash-append redirect, define an exact match for that URI. For broader route matching behavior, compare this with the official Nginx location directive documentation.

Test Nginx proxy_pass Trailing Slash Behavior Safely

A small path-echo backend makes proxy_pass behavior easy to see. Run this in a spare terminal on a non-production system and leave it open while testing:

cat > /tmp/nginx-path-echo.py <<'PY'
from http.server import BaseHTTPRequestHandler, HTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = f"path={self.path}\n".encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, format, *args):
        return

HTTPServer(("127.0.0.1", 9000), Handler).serve_forever()
PY

python3 /tmp/nginx-path-echo.py

The terminal stays attached to the test server. Stop it later with Ctrl+C.

Confirm the echo backend answers directly:

curl http://127.0.0.1:9000/direct-test?active=1

Expected output:

path=/direct-test?active=1

Add Temporary Nginx proxy_pass Test Locations

Place this server block in an included Nginx configuration file on a lab system. On many Fedora, RHEL, Rocky Linux, and nginx.org package installs, /etc/nginx/conf.d/proxy-pass-demo.conf is a practical location. On Debian and Ubuntu package installs, you may prefer /etc/nginx/sites-available/proxy-pass-demo with a symlink into sites-enabled. If you are unsure which layout your package uses, inspect your Nginx includes first:

sudo nginx -T 2>/dev/null | grep -E 'include .*(sites-enabled|conf\.d)'

Relevant output includes one or more include patterns:

include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;

Add this test server block to an included file:

server {
    listen 8080;
    server_name 127.0.0.1;

    location /keep/ {
        proxy_pass http://127.0.0.1:9000;
    }

    location /strip/ {
        proxy_pass http://127.0.0.1:9000/;
    }

    location /replace/ {
        proxy_pass http://127.0.0.1:9000/backend/;
    }
}

Test the configuration, then reload Nginx only if the syntax check passes:

sudo nginx -t
sudo systemctl reload nginx

A successful syntax test prints:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Verify Each Nginx proxy_pass Mapping

The first test keeps the public prefix because proxy_pass has no URI part:

curl http://127.0.0.1:8080/keep/users?active=1

Expected output:

path=/keep/users?active=1

The second test strips the public prefix because proxy_pass includes the URI /:

curl http://127.0.0.1:8080/strip/users?active=1

Expected output:

path=/users?active=1

The third test replaces the public prefix with a backend prefix:

curl http://127.0.0.1:8080/replace/users?active=1

Expected output:

path=/backend/users?active=1

These three checks are enough to confirm the slash behavior before you adapt the pattern to a real application.

Clean Up the Nginx proxy_pass Test Fixture

Remove the temporary server block from whichever included path you used, then test and reload Nginx:

sudo rm -f /etc/nginx/conf.d/proxy-pass-demo.conf
sudo rm -f /etc/nginx/sites-enabled/proxy-pass-demo /etc/nginx/sites-available/proxy-pass-demo
sudo nginx -t && sudo systemctl reload nginx

Stop the Python echo backend with Ctrl+C, then remove the temporary script:

rm -f /tmp/nginx-path-echo.py

Use Nginx proxy_pass with Upstream Blocks

A named upstream group does not change the slash rule. The difference still comes from whether proxy_pass includes a URI part after the upstream name.

upstream app_backend {
    server 127.0.0.1:3000;
    keepalive 16;
}

server {
    listen 80;
    server_name example.com;

    location /api/ {
        proxy_pass http://app_backend/;
        proxy_http_version 1.1;

        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;
    }
}

In this example, /api/users becomes /users upstream because proxy_pass http://app_backend/; includes the URI /. If the backend expects /api/users, change the directive to proxy_pass http://app_backend;.

For multiple backends, load-balancing behavior is handled by the upstream block, while path replacement is still handled by proxy_pass. Keep those two concerns separate when debugging.

Handle Regex Locations and Variables with proxy_pass

Prefix locations are usually easier to maintain for path stripping. Regex locations, named locations, rewritten URIs, and variables can change how Nginx determines the URI to pass upstream. When Nginx cannot determine which exact prefix to replace, a proxy_pass directive with a URI part can fail syntax validation or behave differently than a simple prefix example.

Prefer Prefix Locations for proxy_pass Path Stripping

This prefix location is simple and easy to test:

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}

Use that form before reaching for regex unless the route really needs pattern matching.

Use rewrite with proxy_pass When Regex Is Necessary

If a regex location is unavoidable, keep proxy_pass without a URI part and rewrite the URI deliberately before proxying. This separates the regex work from the upstream selection.

location ~ ^/api/v[0-9]+/(.*)$ {
    rewrite ^/api/v[0-9]+/(.*)$ /$1 break;
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;

    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 a rewrite, test with real paths and logs. Regex routing is more fragile than prefix routing because capture groups, greedy patterns, and missing slashes can change the upstream path. Use the Nginx rewrite rules guide when the rewrite itself becomes the main configuration problem.

Avoid Variables in proxy_pass Unless You Need Them

A named upstream block is usually safer than a variable in proxy_pass. Variables can be valid, but they make URI handling, DNS resolution, and troubleshooting harder to reason about.

# Prefer this for normal reverse proxy routing.
upstream app_backend {
    server 127.0.0.1:3000;
}

server {
    listen 80;
    server_name example.com;

    location /api/ {
        proxy_pass http://app_backend/;
    }
}

Use variables only when the upstream target must be selected dynamically and you have tested the exact resulting URI.

Troubleshoot Nginx proxy_pass Trailing Slash Problems

Most proxy_pass slash mistakes show up as missing routes, doubled prefixes, unexpected redirects, or syntax errors. Start by proving what Nginx sends upstream, then fix the location or proxy_pass URI.

SymptomLikely CauseFirst Check
Backend returns 404 for proxied pathsNginx kept a prefix the backend does not route, or stripped a prefix the backend expects.Send the same path directly to the backend and compare it with the proxied path.
Upstream receives /api/api/usersApplication and Nginx both add the same base path.Check app base URL settings and whether proxy_pass keeps /api/.
/api gives an unexpected response while /api/users worksThe normal slash-append redirect is active, an exact location intentionally overrides it, or the redirect target is not what clients expect.Run curl -I against /api and inspect the status line and Location header.
Nginx reports proxy_pass cannot have URI partThe directive is inside a regex or named location where Nginx cannot determine the replacement prefix.Use a prefix location or rewrite the URI before proxy_pass without a URI part.
Wrong site or default page answersThe request matched another server block or location before it reached the intended proxy.Check server_name, default_server, and location priority.

Fix Upstream 404 Errors After proxy_pass

A proxied 404 often means the backend received a path it does not serve. Test the backend directly from the Nginx host:

curl -i http://127.0.0.1:3000/api/users
curl -i http://127.0.0.1:3000/users

If only /users works directly, strip the public prefix:

location /api/ {
    proxy_pass http://127.0.0.1:3000/;
}

If only /api/users works directly, keep the public prefix:

location /api/ {
    proxy_pass http://127.0.0.1:3000;
}

For broader route problems, use the full Nginx 404 not found troubleshooting workflow.

Fix proxy_pass Syntax Errors in Regex Locations

If nginx -t reports a proxy_pass URI error in a regex location, remove the URI part from proxy_pass or replace the regex with a prefix location. A typical error looks like this:

"proxy_pass" cannot have URI part in location given by regular expression

A safer rewrite pattern keeps the upstream host separate from the URI rewrite:

location ~ ^/api/(.*)$ {
    rewrite ^/api/(.*)$ /$1 break;
    proxy_pass http://127.0.0.1:3000;
}

Run the syntax test again after changing the location:

sudo nginx -t

Fix Wrong Redirects from the Upstream Application

Path mapping and redirects are different problems. proxy_pass decides what URI Nginx sends to the backend. If the backend responds with a redirect to the wrong host or path, inspect the returned Location header:

curl -I http://example.com/api/login

Relevant headers might show the backend redirecting to its internal path:

HTTP/1.1 302 Found
Location: http://127.0.0.1:3000/login

Fix the application base URL first when the framework supports it. Use Nginx proxy_redirect only when the redirect header itself must be rewritten by the proxy. For client-facing redirects that Nginx owns, use the dedicated Nginx URL redirect guide.

Fix 502 Errors After Changing proxy_pass

A slash mistake usually causes a 404 or wrong route, not a 502. If Nginx returns 502 after editing proxy_pass, check whether the upstream address, port, socket, DNS name, or service state changed:

curl -i http://127.0.0.1:3000
sudo journalctl -u nginx --since "10 minutes ago" --no-pager

Use the Nginx 502 Bad Gateway troubleshooting guide when the upstream is unreachable, not just receiving the wrong path.

Use Nginx proxy_pass Trailing Slash Rules Safely

Use these rules when choosing the final form for a production location:

  • Use proxy_pass http://backend; when the backend should receive the original path, including the matched prefix.
  • Use proxy_pass http://backend/; when Nginx should strip the matched prefix and send the remainder from /.
  • Use proxy_pass http://backend/prefix/; when Nginx should replace the public prefix with a different backend prefix.
  • Use slash-terminated prefix locations such as location /api/ for directory-style routes; add an exact location = /api block only when you need to control the bare-prefix response.
  • Prefer simple prefix locations over regex when the goal is only to strip or keep a path prefix.
  • Run sudo nginx -t, reload Nginx, and test with curl before assuming the backend sees the path you intended.

When the route is still confusing, temporarily proxy to a path-echo backend or inspect application logs. Guessing at the slash is slower than proving the exact upstream URI once.

Conclusion

Nginx proxy_pass trailing slash behavior is predictable once the backend path is verified. Keep the URI part empty when the backend expects the public prefix, add / when Nginx should strip that prefix, and use a replacement URI only when the upstream route owns it. A direct backend request plus a proxied curl check catches most route, redirect, and doubled-prefix mistakes.

Follow LinuxCapable

Want more LinuxCapable guides in Google?

Add LinuxCapable as a preferred source so Google can show more of our fresh Linux tutorials in Top Stories and From your sources when relevant.

Add LinuxCapable as a preferred source on Google
Search LinuxCapable

Need another guide?

Search LinuxCapable for package installs, commands, troubleshooting, and follow-up guides related to what you just read.

Found this guide useful?

Support LinuxCapable to keep tutorials free and up to date.

Buy me a coffeeBuy me a coffee
Before commenting, please review our Comments Policy.
Formatting tips for your comment

You can use basic HTML to format your comment. Useful tags currently allowed in published comments:

You type Result
<code>command</code> command
<strong>bold</strong> bold
<em>italic</em> italic
<blockquote>quote</blockquote> quote block

Got a Question or Feedback?

We read and reply to every comment - let us know how we can help or improve this guide.

Let us know you are human: